本地文件同步——C#源代碼


入職之后接到的第一個代碼任務是一個小測試。做一個文件單向同步軟件。


需求描述:

將文件夾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[] 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>

 


效果:

 

希望本篇文章對你有所用處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM