需求描述:

将文件夹A内的文件夹和文件同步到文件夹B。

其实需求也就那么一句话,没啥还需要解释的了吧。详细点说,需要同步文件/文件夹的“新增,删除,重命名,修改”。

一开始我的想法是先Google,然后在博客园找到这篇文章《C#文件同步工具教程》。这篇文章的核心来自msdn里面FileSystemWatcher 的解释。就是用对象FileSystemWatcher 去监听文件是否被创建,重命名,删除,修改。如果发生了就调用相对应的事件,将被修改,创建,重命名的文件复制到目标目录B当中。这个例子比较简单,很多事情都没考虑到。而且我认为用FileSystemWatcher 去监听所有的文件,太浪费CPU和内存。


我的想法

是采用递归,遍历整个源目录,对比目标目录。

    1. 如果目标目录下没有相对应的文件,将文件复制到目标目录;
    2. 如果文件在两个路径下都存在,但是文件大小和最后写入时间不一致时,将原目录下的文件复制到目标目录下;
    3. 如果文件存在于目标目录下而不存在源目录下,则将目标路径下的文件删除。

实现

知道如何比较之后就可以进行递归遍历文件夹了。这个是这个软件实现的难点之一,其实也没多难,也就是说这个软件根本就没多难。以下是递归函数:

复制代码
 1 /// <summary>  2 /// 递归核心 同步目录   3 /// </summary>  4 /// <param name="src">原路径</param>  5 /// <param name="obj">目标路径</param>  6 static void loop(string src, string obj)    // 递归核心 同步目录   7 {  8     CopyFistly(src, obj);   //先同步文件  9  10     //遍历文件夹,递归调用 11     DirectoryInfo dirSrc = new DirectoryInfo(src); 12     DirectoryInfo[] dirs = dirSrc.GetDirectories(); 13     foreach (DirectoryInfo dir in dirs) 14     { 15         string str = dir.Name; 16         if (Directory.Exists(obj + "\\" + dir.Name) == false) 17         { 18             str = Directory.CreateDirectory(obj + "\\" + dir.Name).ToString(); 19         } 20         //注意这里,这里是递归,而且是下面要注意的地方 21         loop(src + "\\" + dir.ToString(), obj + "\\" + str);     22     } 23 }
复制代码

测试了一下结果,在9000+个文件,40+个文件夹下,在我这部破机器上面单纯递归遍历(不复制文件)的时候需要的时间是截枝的十倍以上。简直是只乌龟。。。


优化

所以要想办法缩短时间提高效率。既然复制文件上面我们无法操作,那我们只好在递归上面进行优化。上个星期我发了一篇文章叫做《算法——回溯法》。这个时候刚好可以用上这种方法了。因为本身用的就是递归,而且文件夹的结构本身就是一个树的结构,在恰好满足了回溯法的要求。在遍历上面,并不需要在所有的文件夹都遍历一遍。因为有些文件夹并没有发生改变,所有就没有必要遍历下去了。所以就需要在递归调用自己之前先加一个条件,也就是加上约束函数。修改之后,代码如下:

复制代码
 1 /// <summary>  2 /// 递归核心 同步目录   3 /// </summary>  4 /// <param name="src">原路径</param>  5 /// <param name="obj">目标路径</param>  6 static void loop(string src, string obj)    // 递归核心 同步目录   7 {  8     CopyFistly(src, obj);   //先同步文件  9  10     //遍历文件夹,递归调用 11     DirectoryInfo dirSrc = new DirectoryInfo(src); 12     DirectoryInfo[] dirs = dirSrc.GetDirectories(); 13     foreach (DirectoryInfo dir in dirs) 14     { 15         string str = dir.Name; 16         if (Directory.Exists(obj + "\\" + dir.Name) == false) 17         { 18             str = Directory.CreateDirectory(obj + "\\" + dir.Name).ToString(); 19         } 20         DirectoryInfo dirObj = new DirectoryInfo(str); 21         //约束函数 在大小不一致的时候进行同步,其他状态不同步 22         if (GetDirectoryLength(src + "\\" + dir.ToString()) != GetDirectoryLength(obj + "\\" + str))     23             loop(src + "\\" + dir.ToString(), obj + "\\" + str); 24     } 25 }
复制代码

函数GetDirectoryLength(string path)的作用是检查文件夹path的大小。这里只是简单地对比两个文件夹的大小,如果大小一致,则截枝不递归,否则递归。这种方式的效率非常高,因为很多时候并不是都在有文件的复制,所以不需要经常去遍历目录。所以截枝就好了。下面给出GetDirectoryLength(string path)函数的代码。其实该函数也是一个递归,虽然会增加负荷,但是文件多,文件夹深的时候,是很有必要的。

获取文件夹大小
复制代码
 1 /// <summary>  2 /// 获取路径下文件夹的大小  3 /// </summary>  4 /// <param name="dirPath">目标路径</param>  5 /// <returns>文件夹大小</returns>  6 public static long GetDirectoryLength(string dirPath)  7 {  8     //判断给定的路径是否存在,如果不存在则退出  9     if (!Directory.Exists(dirPath)) 10         return 0; 11     long len = 0; 12  13     //定义一个DirectoryInfo对象 14     DirectoryInfo di = new DirectoryInfo(dirPath); 15  16     //通过GetFiles方法,获取di目录中的所有文件的大小 17     foreach (FileInfo fi in di.GetFiles()) 18     { 19         len += fi.Length; 20     } 21  22     //获取di中所有的文件夹,并存到一个新的对象数组中,以进行递归 23     DirectoryInfo[] dis = di.GetDirectories(); 24     if (dis.Length > 0) 25     { 26         for (int i = 0; i < dis.Length; i++) 27         { 28             len += GetDirectoryLength(dis[i].FullName); 29         } 30     } 31     return len; 32 }
复制代码

难点主要在递归和截枝的思想上面。其他方面的解释可以直接查看代码。注释已经很清楚了。下面是整个文件的源代码:

文件同步
复制代码
  1 using System;   2 using System.Collections.Generic;   3 using System.Linq;   4 using System.Text;   5 using System.IO;   6 using System.Threading;   7 using System.Configuration;   8    9 namespace FileSynLoop  10 {  11     class Program  12     {  13         static private string strSource = GetAppConfig("src");   //原路径  14         static private string strObjective = GetAppConfig("obj");    //目标路径  15         static private int synTime = 0;  //同步时间  16         static private string flag = ""; //多线程控制标志 同步控制  17         static private Thread threadShow;   //显示效果线程  18         static private bool bound;  //是否使用截枝函数  19   20         static void Main(string[] args)  21         {  22             //基本设置  23             Console.WriteLine("原路径:" + strSource);  24             Console.WriteLine("目标路径:" + strObjective);  25             try { synTime = Convert.ToInt32(GetAppConfig("synTime")); }  26             catch (Exception e) { Console.WriteLine("配置的同步时间格式不正确:" + e.Message); return; }  27             Console.WriteLine("同步间隔时间:" + synTime + "毫秒");  28   29             if (Directory.Exists(strSource) == false){Console.WriteLine("配置的原路径不存在");return;}  30             if (Directory.Exists(strObjective) == false){Console.WriteLine("配置的目标路径不存在"); return;}  31   32             Console.WriteLine("是否使用截枝函数?使用截止函数无法同步空文件夹/空文件。默认使用截枝!y/n");  33             if (Console.ReadLine() == "n")  34                 bound = false;  35             else  36                 bound = true;  37   38             do{Console.WriteLine("输入ok开始!");}  39             while (Console.ReadLine() != "ok");  40   41             //线程  42             Thread thread = new Thread(new ThreadStart(ThreadProc));  43             threadShow = new Thread(new ThreadStart(ThreadShow));  44             thread.Start();  45             threadShow.Start(); //开始线程  46             threadShow.Suspend();   //挂起线程  47             //退出  48             while ((flag = Console.ReadLine()) != "exit") ;  49         }  50   51         //线程控制  52         public static void ThreadProc()  53         {  54             int i = 0;  55             DateTime dt;  56             TimeSpan ts;  57   58             while (flag != "exit")  59             {  60                 dt = DateTime.Now;  61                 Console.WriteLine();  62                 Console.Write("第" + ++i + "次同步开始:");  63                 threadShow.Resume();    //恢复线程  64                 try  65                 {  66                     loop(strSource, strObjective);  67                 }  68                 catch (Exception e)  69                 {  70                     Console.WriteLine("文件夹“" + strSource + "“被占用,暂时无法同步!8:" + e.Message);  71                 }  72                 threadShow.Suspend();   //挂起线程  73   74                 ts = DateTime.Now - dt;  75                 Console.WriteLine("|");  76                 if (GetDirectoryLength(strSource) == GetDirectoryLength(strObjective))  77                     Console.WriteLine("所有同步完毕!");  78                 Console.WriteLine("第" + i + "次同步结束,耗时"+ts.ToString()+",正在等待下次开始!");  79                 Thread.Sleep(synTime);//同步时间  80             }  81         }  82   83         //显示效果的线程  84         public static void ThreadShow()  85         {  86             while (flag != "exit")  87             {  88                 Console.Write(">");  89                 Thread.Sleep(500);  90             }  91         }  92   93         /// <summary>  94         /// 递归核心 同步目录   95         /// </summary>  96         /// <param name="src">原路径</param>  97         /// <param name="obj">目标路径</param>  98         static void loop(string src, string obj)    // 递归核心 同步目录   99         { 100             CopyFistly(src, obj);   //先同步文件 101  102             //遍历文件夹,递归调用 103             DirectoryInfo dirSrc = new DirectoryInfo(src); 104             DirectoryInfo[] dirs = dirSrc.GetDirectories(); 105             foreach (DirectoryInfo dir in dirs) 106             { 107                 string str = dir.Name; 108                 if (Directory.Exists(obj + "\\" + dir.Name) == false) 109                 { 110                     str = Directory.CreateDirectory(obj + "\\" + dir.Name).ToString(); 111                 } 112                 DirectoryInfo dirObj = new DirectoryInfo(str); 113                 if (bound) 114                 { 115                     //约束函数 在大小不一致的时候进行同步,其他状态不同步 116                     if (GetDirectoryLength(src + "\\" + dir.ToString()) != GetDirectoryLength(obj + "\\" + str))     117                         loop(src + "\\" + dir.ToString(), obj + "\\" + str); 118                 } 119                 else 120                 { 121                     loop(src + "\\" + dir.ToString(), obj + "\\" + str); 122                 } 123             } 124         } 125  126         /// <summary> 127         /// 同步文件 128         /// </summary> 129         /// <param name="strSource">源目录</param> 130         /// <param name="strObjective">目标目录</param> 131         static private void CopyFistly(string strSource, string strObjective)   //同步文件 132         { 133             string[] srcFileNames = Directory.GetFiles(strSource).Select(s => System.IO.Path.GetFileName(s)).ToArray(); //原路径下的所有文件 134             string[] objFileNames = Directory.GetFiles(strObjective).Select(s => System.IO.Path.GetFileName(s)).ToArray();  //目标路径下的所有文件 135  136             #region 同步新建 修改 137             foreach (string strSrc in srcFileNames) //遍历源文件夹 138             { 139                 FileInfo aFile = new FileInfo(strSource + "\\" + strSrc); 140                 string aAccessTime = aFile.LastWriteTime.ToString(); 141                 string aCreateTime = aFile.CreationTime.ToString(); 142  143  144                 string bCreateTime = "";    //目标路径文件的信息 145                 string bAccessTime = ""; 146  147                 bool flag = false; 148                 foreach (string strObj in objFileNames) //遍历目标文件夹 149                 { 150                     FileInfo bFile = new FileInfo(strObjective + "\\" + strObj); 151                     bAccessTime = bFile.LastWriteTime.ToString(); 152                     bCreateTime = bFile.CreationTime.ToString(); 153  154                     if (strSrc == strObj)   //文件存在目标路径当中 155                     { 156                         if (aCreateTime != bCreateTime || aAccessTime != bAccessTime)   //文件存在但是不一致 157                         { 158                             try 159                             { 160                                 File.Copy(strSource + "\\" + strSrc, strObjective + "\\" + strSrc, true); 161                                 FileInfo file = new FileInfo(strObjective + "\\" + strSrc); 162                                 file.CreationTime = Convert.ToDateTime(aCreateTime); 163                                 file.LastAccessTime = Convert.ToDateTime(aAccessTime); 164                             } 165                             catch (Exception e) 166                             { 167                                 Console.WriteLine("文件“" + strSrc + "“被占用,暂时无法同步!4:" + e.Message); 168                             } 169                         } 170                         flag = true; 171                         break; 172                     } 173                 } 174  175                 if (flag == false)  //文件不存在目标路径当中 176                 { 177                     try 178                     { 179                         File.Copy(strSource + "\\" + strSrc, strObjective + "\\" + strSrc, true); 180                         FileInfo file = new FileInfo(strObjective + "\\" + strSrc); 181                         file.CreationTime = Convert.ToDateTime(aCreateTime); 182                         file.LastAccessTime = Convert.ToDateTime(aAccessTime); 183                     } 184                     catch (Exception e) 185                     { 186                         Console.WriteLine("文件“" + strSrc + "“被占用,暂时无法同步!5" + e.Message); 187                     } 188                 } 189             } 190             #endregion 191  192             #region 同步删除 重命名 193             //删除文件 194             foreach (string strObj in objFileNames) //遍历目标文件夹 195             { 196                 string allObj = strObjective + "\\" + strObj; 197                 bool flag = false; 198                 foreach (string strSrc in srcFileNames) //遍历源文件夹 199                 { 200                     if (strObj == strSrc) 201                         flag = true; 202                 } 203                 if (flag == false) 204                 { 205                     try 206                     { 207                         File.Delete(allObj); 208                     } 209                     catch (Exception e) 210                     { 211                         Console.WriteLine("文件“" + strObj + "“被占用,暂时无法同步!6" + e.Message); 212                     } 213                 } 214             } 215  216             //删除文件夹 217             DirectoryInfo dirSrc = new DirectoryInfo(strSource); 218             DirectoryInfo[] dir***c = dirSrc.GetDirectories(); 219             DirectoryInfo dirObj = new DirectoryInfo(strObjective); 220             DirectoryInfo[] dirsObj = dirObj.GetDirectories(); 221             foreach (DirectoryInfo bdirObj in dirsObj) 222             { 223                 bool flag = false; 224                 foreach (DirectoryInfo adirSrc in dir***c) 225                 { 226                     if (bdirObj.Name == adirSrc.Name) 227                     { 228                         flag = true; 229                     } 230                 } 231                 if (flag == false)  //如果文件夹只出现在目的路径下而不再源目录下,删除该文件夹 232                 { 233                     try 234                     { 235                         Directory.Delete(dirObj + "\\" + bdirObj, true); 236                     } 237                     catch (Exception e) 238                     { 239                         Console.WriteLine("文件夹“" + bdirObj + "“被占用,暂时无法同步!8" + e.Message); 240                     } 241                 } 242             } 243             #endregion 244  245         } 246  247         /// <summary> 248         /// 获取自定义配置的值 249         /// </summary> 250         /// <param name="strKey">键值</param> 251         /// <returns>键值对应的值</returns> 252         private static string GetAppConfig(string strKey) 253         { 254             foreach (string key in ConfigurationManager.AppSettings) 255             { 256                 if (key == strKey) 257                 { 258                     return ConfigurationManager.AppSettings[strKey]; 259                 } 260             } 261             return null; 262         } 263  264         /// <summary>  265         /// 获取路径下文件夹的大小 266         /// </summary> 267         /// <param name="dirPath">目标路径</param> 268         /// <returns>文件夹大小</returns> 269         public static long GetDirectoryLength(string dirPath) 270         { 271             //判断给定的路径是否存在,如果不存在则退出 272             if (!Directory.Exists(dirPath)) 273                 return 0; 274             long len = 0; 275  276             //定义一个DirectoryInfo对象 277             DirectoryInfo di = new DirectoryInfo(dirPath); 278  279             //通过GetFiles方法,获取di目录中的所有文件的大小 280             foreach (FileInfo fi in di.GetFiles()) 281             { 282                 len += fi.Length; 283             } 284  285             //获取di中所有的文件夹,并存到一个新的对象数组中,以进行递归 286             DirectoryInfo[] dis = di.GetDirectories(); 287             if (dis.Length > 0) 288             { 289                 for (int i = 0; i < dis.Length; i++) 290                 { 291                     len += GetDirectoryLength(dis[i].FullName); 292                 } 293             } 294             return len; 295         } 296     } 297 }
复制代码


效果: