入職之后接到的第一個代碼任務是一個小測試。做一個文件單向同步軟件。
需求描述:
將文件夾A內的文件夾和文件同步到文件夾B。
其實需求也就那么一句話,沒啥還需要解釋的了吧。詳細點說,需要同步文件/文件夾的“新增,刪除,重命名,修改”。
一開始我的想法是先Google,然后在博客園找到這篇文章《C#文件同步工具教程》。這篇文章的核心來自msdn里面FileSystemWatcher 的解釋。就是用對象FileSystemWatcher 去監聽文件是否被創建,重命名,刪除,修改。如果發生了就調用相對應的事件,將被修改,創建,重命名的文件復制到目標目錄B當中。這個例子比較簡單,很多事情都沒考慮到。而且我認為用FileSystemWatcher 去監聽所有的文件,太浪費CPU和內存。
我的想法
是采用遞歸,遍歷整個源目錄,對比目標目錄。
-
- 如果目標目錄下沒有相對應的文件,將文件復制到目標目錄;
- 如果文件在兩個路徑下都存在,但是文件大小和最后寫入時間不一致時,將原目錄下的文件復制到目標目錄下;
- 如果文件存在於目標目錄下而不存在源目錄下,則將目標路徑下的文件刪除。
實現
知道如何比較之后就可以進行遞歸遍歷文件夾了。這個是這個軟件實現的難點之一,其實也沒多難,也就是說這個軟件根本就沒多難。以下是遞歸函數:
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[] dirsSrc = 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 dirsSrc) 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 }
配置文件的代碼
因為要求用配置文件,配置原路徑,目的路徑等信息,有必要說明一下這個問題。另外需要讀配置文件,所以需要引用System.Configuration;命名空間。一下是配置文件app.config代碼:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <appSettings> <!--原路徑--> <add key="src" value="e:\test\a"/> <!--目標路徑--> <add key="obj" value="e:\test\b"/> <!--日記文件路徑--> <add key="logs" value="e:\test\logs.txt"/> <!--自動同步時間,單位為毫秒--> <add key="synTime" value="5000"/> </appSettings> </configuration>
效果:
希望本篇文章對你有所用處。