Prepare
本文將使用一個NuGet公開的組件技術來實現一個服務器端的文件管理引擎,提供了一些簡單的API,來方便的實現文件引擎來對您自己的軟件系統的文件進行管理。
聯系作者及加群方式(激活碼在群里發放):http://www.hslcommunication.cn/Cooperation
在Visual Studio 中的NuGet管理器中可以下載安裝,也可以直接在NuGet控制台輸入下面的指令安裝:
Install-Package HslCommunication
NuGet安裝教程 http://www.cnblogs.com/dathlin/p/7705014.html
Summary
這個文件管理的引擎實現的功能是對所有客戶端上傳的文件信息進行管理,客戶端在上傳或是下載的時候允許進度報告。如果我們只是顯示一個文件發送到服務器上,服務器接收數據后保存到本地,那么這是非常容易實現的,只要比較熟悉網絡通信就可以,但是對於文件服務器引擎需要的邏輯更多,允許上傳額外的信息,包括文件的上傳人,上傳日期,下載次數等等信息,然后允許上傳的時候不影響下載,可以同時下載,同時上傳,而服務器的硬盤IO不進行阻塞,這樣實現起來就相當困難了,但是上述所有的功能在使用本組件實現的時候就非常的方便,當客戶端進行上傳下載的時候更是調用一個方法就能完成。
本文件引擎的特色是實現了一個文件的讀寫分離,無鎖讀寫,也就是說,一個文件內容支持同時下載,同時上傳,甚至下載的時候,進行上傳,原有的下載不會受影響。所有的上傳,下載,刪除都是線程安全的,無論在哪個線程都是方便調用的。
需求場景:
- 比如我們要開發一個項目管理系統,如果我們想要實現每個項目允許上傳附件,需要支持方便的下載,刪除,上傳操作。
- 比如我們開發一個設備資料管理系統,除了設備一些自帶屬性需要創建關系型數據表外,還要支持附件管理。
- 比如我們需要實現一個軟件的共享文件管理器,在你軟件的首頁上支持方便的顯示。
- 再比如個人賬戶的附件管理,個人頭像管理等等。
一個基於本組件擴展出來的CS架構的基礎模版項目,二次基於此進行方便的二次開發,該項目使用了好幾處的文件管理:
https://github.com/dathlin/ClientServerProject
一個C-S模版,該模版由三部分的程序組成,一個服務端運行的程序,一個客戶端運行的程序,還有一個公共的組件,實現了基礎的賬戶管理功能,版本控制,軟件升級,公告管理,消息群發,共享文件上傳下載,批量文件傳送功能。具體的操作方法見演示就行。本項目的一個目標是:提供一個基礎的中小型系統的C-S框架,客戶端有四種模式,無縫集成訪問,winform版本,wpf版本,asp.net mvc版本,Android版本。方便企業進行中小型系統的二次開發和個人學習。
Reference
日志組件所有的功能類都在 HslCommunication 和 HslCommunication.Enthernet 命名空間,所以再使用之前先添加
using HslCommunication; using HslCommunication.Enthernet;
How to Use
首先先要在服務器端進行搭建服務,基本上只需要兩個參數即可,端口號和文件引擎的基礎路徑。至於日志,可要可不要,看自己的需求,系統會對文件的下載,上傳,異常情況進行記錄。
我們先上服務器的代碼,假設需要日志記錄,如果你不需要的話,就注釋掉那兩行日志相關的代碼,系統也可以實現對指定分類的文件數量進行監視,當數量變化的時候進行更新推送等等。ok,接下來先看看一種最簡單的方式。
private UltimateFileServer ultimateFileServer; // 引擎對象 private void UltimateFileServerInitialization() { ultimateFileServer = new UltimateFileServer(); // 實例化對象 ultimateFileServer.FilesDirectoryPath = Application.StartupPath + @"\UltimateFile"; // 所有文件存儲的基礎路徑 ultimateFileServer.ServerStart(34567); // 啟動一個端口的引擎 } private void userButton1_Click(object sender, EventArgs e) { // 點擊了啟動服務器端的文件引擎 UltimateFileServerInitialization(); userButton1.Enabled = false; }
聲明一個服務器端的對象,然后寫一個初始化方法來實例化數據,然后在一個按鈕中調用這個初始化方法即可。服務器端的程序簡單版就只有這么多了。
客戶端操作:
實例化:
客戶端在進行文件的上傳,下載之前先進行客戶端的實例化,實例化的時候可以指定一些信息,目前測試來說,不需要。
#region 客戶端核心引擎 private IntegrationFileClient integrationFileClient; // 客戶端的核心引擎 private void IntegrationFileClientInitialization() { // 定義連接服務器的一些屬性,超時時間,IP及端口信息 integrationFileClient = new IntegrationFileClient() { ConnectTimeout = 5000, ServerIpEndPoint = new System.Net.IPEndPoint(System.Net.IPAddress.Parse("127.0.0.1"), 34567), }; // 創建本地文件存儲的路徑 string path = Application.StartupPath + @"\Files"; if(!System.IO.Directory.Exists(path)) { System.IO.Directory.CreateDirectory(path); } } #endregion
上傳文件:
在講解上傳文件操作之前,先說明下本文件管理引擎的機制,對於每個文件,都有三個分類機制,用於文件的分類,以及標注在服務器端的文件存儲路徑。當你確認上傳一個文件后,需要確認3個分類目錄和文件在服務器端真正存儲的文件名,一般就是文件自己的名稱。接下來我們來上傳一個文件吧,該文件來自手動選擇:
#region 上傳文件塊 /************************************************************************************************* * * 一條指令即可完成文件的上傳操作,上傳模式有三種 * 1. 指定本地的完整路徑的文件名 * 2. 將流(stream)中的數據上傳到服務器 * 3. 將bitmap圖片數據上傳到服務器 * ********************************************************************************************/ private void userButton3_Click(object sender, EventArgs e) { // 點擊后進行文件選擇 using (OpenFileDialog ofd = new OpenFileDialog()) { if (ofd.ShowDialog() == DialogResult.OK) { textBox1.Text = ofd.FileName; } } } private void userButton2_Click(object sender, EventArgs e) { if (!string.IsNullOrEmpty(textBox1.Text)) { // 點擊開始上傳,此處按照實際項目需求放到了后台線程處理,事實上這種耗時的操作就應該放到后台線程 System.Threading.Thread thread = new System.Threading.Thread(new System.Threading.ParameterizedThreadStart((ThreadUploadFile))); thread.IsBackground = true; thread.Start(textBox1.Text); userButton2.Enabled = false; progressBar1.Value = 0; } } private void ThreadUploadFile(object filename) { if (filename is string fileName) { System.IO.FileInfo fileInfo = new System.IO.FileInfo(fileName); // 開始正式上傳,關於三級分類,下面只是舉個例子,上傳成功后去服務器端尋找文件就能明白 OperateResult result = integrationFileClient.UploadFile( fileName, // 需要上傳的原文件的完整路徑,上傳成功還需要個條件,該文件不能被占用 fileInfo.Name, // 在服務器存儲的文件名,帶后綴,一般設置為原文件的文件名 "Files", // 第一級分類,指示文件存儲的類別,對應在服務器端存儲的路徑不一致 "Personal", // 第二級分類,指示文件存儲的類別,對應在服務器端存儲的路徑不一致 "Admin", // 第三級分類,指示文件存儲的類別,對應在服務器端存儲的路徑不一致 "這個文件非常重要", // 這個文件的額外描述文本,可以為空("") "張三", // 文件的上傳人,當然你也可以不使用 UpdateReportProgress // 文件上傳時的進度報告,如果你不需要,指定為NULL就行,一般文件比較大,帶寬比較小,都需要進度提示 ); // 切換到UI前台顯示結果 Invoke(new Action<OperateResult>(operateResult => { userButton2.Enabled = true; if (result.IsSuccess) { MessageBox.Show("文件上傳成功!"); } else { // 失敗原因多半來自網絡異常,還有文件不存在,分類名稱填寫異常 MessageBox.Show("文件上傳失敗:" + result.ToMessageShowString()); } }), result); } } /// <summary> /// 用於更新上傳進度的方法,該方法是線程安全的 /// </summary> /// <param name="sended">已經上傳的字節數</param> /// <param name="totle">總字節數</param> private void UpdateReportProgress(long sended, long totle) { if (progressBar1.InvokeRequired) { progressBar1.Invoke(new Action<long, long>(UpdateReportProgress), sended, totle); return; } // 此處代碼是安全的 int value = (int)(sended * 100L / totle); progressBar1.Value = value; } #endregion
在文件上傳的時候可以指定一些額外的信息,比如說文件上傳人,額外的描述文本。
下載文件:
#region 文件下載塊 /************************************************************************************************* * * 一條指令即可完成文件的下載操作,下載模式有三種 * 1. 指定需要下載的文件名(帶后綴) * 2. 將服務器上的數據下載到流(stream)中 * 3. 將服務器上的數據下載到bitmap圖片中 * ********************************************************************************************/ /// <summary> /// 點擊了文件下載觸發的事件,如果需要下載一個文件,要傳入下載文件的完整名稱 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void userButton5_Click(object sender, EventArgs e) { // 點擊開始下載,此處按照實際項目需求放到了后台線程處理,事實上這種耗時的操作就應該放到后台線程 System.Threading.Thread thread = new System.Threading.Thread(new System.Threading.ParameterizedThreadStart((ThreadDownloadFile))); thread.IsBackground = true; thread.Start(textBox2.Text); progressBar1.Value = 0; } private void ThreadDownloadFile(object filename) { if (filename is string fileName) { OperateResult result = integrationFileClient.DownloadFile( fileName, // 文件在服務器上保存的名稱,舉例123.txt "Files", // 第一級分類,指示文件存儲的類別,對應在服務器端存儲的路徑不一致 "Personal", // 第二級分類,指示文件存儲的類別,對應在服務器端存儲的路徑不一致 "Admin", // 第三級分類,指示文件存儲的類別,對應在服務器端存儲的路徑不一致 DownloadReportProgress, // 文件下載的時候的進度報告,友好的提示下載進度信息 Application.StartupPath + @"\Files\" + filename // 下載后在文本保存的路徑,也可以直接下載到 MemoryStream 的數據流中,或是bitmap中 ); // 切換到UI前台顯示結果 Invoke(new Action<OperateResult>(operateResult => { if (result.IsSuccess) { MessageBox.Show("文件下載成功!"); } else { // 失敗原因多半來自網絡異常,還有文件不存在,分類名稱填寫異常 MessageBox.Show("文件下載失敗:" + result.ToMessageShowString()); } }), result); } } /// <summary> /// 用於更新文件下載進度的方法,該方法是線程安全的 /// </summary> /// <param name="receive">已經接收的字節數</param> /// <param name="totle">總字節數</param> private void DownloadReportProgress(long receive, long totle) { if (progressBar2.InvokeRequired) { progressBar2.Invoke(new Action<long, long>(DownloadReportProgress), receive, totle); return; } // 此處代碼是安全的 int value = (int)(receive * 100L / totle); progressBar2.Value = value; } #endregion
文件刪除:
#region 文件的刪除操作 private void userButton1_Click(object sender, EventArgs e) { // 文件的刪除不需要放在后台線程,前台即可處理,無論多少大的文件,無論該文件是否在下載中,都是很快刪除的 OperateResult result = integrationFileClient.DeleteFile("123.txt", "Files", "Personal", "Admin"); if(result.IsSuccess) { MessageBox.Show("文件刪除成功!"); } else { // 刪除失敗的原因除了一般的網絡問題,還有因為服務器的文件不存在,會在Message里有顯示。 MessageBox.Show("文件刪除失敗,原因:" + result.ToMessageShowString()); } } #endregion
獲取服務器的文件信息:
上面的信息操作,都是指定了三個文件夾路徑,上面的示例是:"Files","Personal","Admin" ,比如我們向這個文件夾里上傳了很多文件,想知道文件夾里有什么文件,以及他們的詳細信息,可以調用如下的方法:
private void userButton4_Click(object sender, EventArgs e) { // 獲取服務器指定目錄的所有文件 OperateResult result = integrationFileClient.DownloadPathFileNames(out GroupFileItem[] files, "Files", "Personal", "Admin"); if(result.IsSuccess) { treeView1.Nodes[0].Nodes.Clear(); foreach(var file in files) { TreeNode node = new TreeNode(file.FileName); node.Tag = file; treeView1.Nodes[0].Nodes.Add(node); } treeView1.Nodes[0].Expand(); } else { // 獲取文件名失敗 MessageBox.Show(result.ToMessageShowString()); } }
獲取了文件信息,並存儲在了數組files中,當我們點擊了這個樹節點的時候,就顯示了文件的詳細信息:
private void treeView1_AfterSelect(object sender, TreeViewEventArgs e) { TreeNode node = e.Node; if (node.Text != "文件列表") { textBox2.Text = node.Text; if(node.Tag is GroupFileItem item) { StringBuilder info = new StringBuilder(); info.Append("文件名:"); info.Append(item.FileName); info.Append(Environment.NewLine); info.Append("文件大小:"); info.Append(item.FileSize); info.Append(Environment.NewLine); info.Append("文件描述:"); info.Append(item.Description); info.Append(Environment.NewLine); info.Append("上傳人:"); info.Append(item.Owner); info.Append(Environment.NewLine); info.Append("上傳時間:"); info.Append(item.UploadTime.ToString()); info.Append(Environment.NewLine); info.Append("下載次數:"); info.Append(item.DownloadTimes); info.Append(Environment.NewLine); textBox3.Text = info.ToString(); } } }
獲取服務器的文件信息:
private void userButton6_Click(object sender, EventArgs e) { // 獲取服務器指定目錄的所有文件 OperateResult result = integrationFileClient.DownloadPathFolders(out string[] folders, "Files", "Personal", ""); if (result.IsSuccess) { treeView1.Nodes[0].Nodes.Clear(); foreach (var fold in folders) { TreeNode node = new TreeNode(fold); treeView1.Nodes[0].Nodes.Add(node); } treeView1.Nodes[0].Expand(); } else { // 獲取文件名失敗 MessageBox.Show(result.ToMessageShowString()); } }
這個用於獲取到服務器在Files/Personal/目錄下面的子文件名稱,之前的存儲就是有一個Admin
高級應用:
配置網絡令牌
按上述搭建的服務器,可以輕松被客戶端訪問到,我們也沒有輸入過密碼之類的東西,只要知道ip及端口,其他程序也可以訪問你的數據,為了安全起見,允許在服務器端設置令牌,來加強安全,服務器端的代碼如下:
private void UltimateFileServerInitialization() { ultimateFileServer = new UltimateFileServer(); // 實例化對象 ultimateFileServer.KeyToken = new Guid("A8826745-84E1-4ED4-AE2E-D3D70A9725B5"); ultimateFileServer.FilesDirectoryPath = Application.StartupPath + @"\UltimateFile"; // 所有文件存儲的基礎路徑 ultimateFileServer.ServerStart(34567); // 啟動一個端口的引擎 }
然后客戶端在實例化的時候,也需要指定一樣的令牌,否則無法訪問數據。
private void IntegrationFileClientInitialization() { // 定義連接服務器的一些屬性,超時時間,IP及端口信息 integrationFileClient = new IntegrationFileClient() { ConnectTimeout = 5000, ServerIpEndPoint = new System.Net.IPEndPoint(System.Net.IPAddress.Parse("127.0.0.1"), 34567), KeyToken = new Guid("A8826745-84E1-4ED4-AE2E-D3D70A9725B5") // 指定一個令牌 }; // 創建本地文件存儲的路徑 string path = Application.StartupPath + @"\Files"; if (!System.IO.Directory.Exists(path)) { System.IO.Directory.CreateDirectory(path); } }
配置日志記錄:
主要用於服務器端的日志記錄:
private void UltimateFileServerInitialization() { ultimateFileServer = new UltimateFileServer(); // 實例化對象 ultimateFileServer.KeyToken = new Guid("A8826745-84E1-4ED4-AE2E-D3D70A9725B5"); // 指定一個令牌 ultimateFileServer.LogNet = new HslCommunication.LogNet.LogNetSingle(Application.StartupPath + @"\Logs\123.txt"); ultimateFileServer.FilesDirectoryPath = Application.StartupPath + @"\UltimateFile"; // 所有文件存儲的基礎路徑 ultimateFileServer.ServerStart(34567); // 啟動一個端口的引擎 }
指定目錄下的文件數量監視:
比如我們需要實現的功能,對這個目錄下的("Files", "Personal", "Admin")文件數量進行監視,當有文件上傳,刪除時,進行觸發消息,如下的示例演示,在服務器端界面新增一個數據,顯示這個目錄的文件總數量信息。
private void UltimateFileServerInitialization() { ultimateFileServer = new UltimateFileServer(); // 實例化對象 ultimateFileServer.KeyToken = new Guid("A8826745-84E1-4ED4-AE2E-D3D70A9725B5"); // 指定一個令牌 ultimateFileServer.LogNet = new HslCommunication.LogNet.LogNetSingle(Application.StartupPath + @"\Logs\123.txt"); ultimateFileServer.FilesDirectoryPath = Application.StartupPath + @"\UltimateFile"; // 所有文件存儲的基礎路徑 ultimateFileServer.ServerStart(34567); // 啟動一個端口的引擎 // 訂閱一個目錄的信息,使用文件集容器實現 GroupFileContainer container = ultimateFileServer.GetGroupFromFilePath(Application.StartupPath + @"\UltimateFile\Files\Personal\Admin"); container.FileCountChanged += Container_FileCountChanged; // 當文件數量發生變化時觸發 } private void Container_FileCountChanged(int obj) { if (InvokeRequired) { Invoke(new Action<int>(Container_FileCountChanged), obj); return; } label1.Text = "文件數量:" + obj.ToString(); }
示例界面:
該項目的源代碼公開,請遵循MIT協議,地址為:https://github.com/dathlin/FileManagment