C# 文件上传下载功能实现 文件管理引擎开发
2021-07-19 19:22
Prepare
本文将使用一个NuGet公开的组件技术来实现一个服务器端的文件管理引擎,提供了一些简单的API,来方便的实现文件引擎来对您自己的软件系统的文件进行管理。
在Visual Studio 中的NuGet管理器中可以下载安装,也可以直接在NuGet控制台输入下面的指令安装:
Install-Package HslCommunication
NuGet安装教程 http://www.cnblogs.com/dathlin/p/7705014.html
技术支持QQ群:592132877 (组件的版本更新细节也将第一时间在群里发布)
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 => { userButton2.Enabled = true; if (result.IsSuccess) { MessageBox.Show("文件上传成功!"); } else { // 失败原因多半来自网络异常,还有文件不存在,分类名称填写异常 MessageBox.Show("文件上传失败:" + result.ToMessageShowString()); } }), result); } } /// /// 用于更新上传进度的方法,该方法是线程安全的 /// /// 已经上传的字节数 /// 总字节数 private void UpdateReportProgress(long sended, long totle) { if (progressBar1.InvokeRequired) { progressBar1.Invoke(new Action(UpdateReportProgress), sended, totle); return; } // 此处代码是安全的 int value = (int)(sended * 100L / totle); progressBar1.Value = value; } #endregion
在文件上传的时候可以指定一些额外的信息,比如说文件上传人,额外的描述文本。
下载文件:
#region 文件下载块 /************************************************************************************************* * * 一条指令即可完成文件的下载操作,下载模式有三种 * 1. 指定需要下载的文件名(带后缀) * 2. 将服务器上的数据下载到流(stream)中 * 3. 将服务器上的数据下载到bitmap图片中 * ********************************************************************************************/ ////// 点击了文件下载触发的事件,如果需要下载一个文件,要传入下载文件的完整名称 /// /// /// 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 => { if (result.IsSuccess) { MessageBox.Show("文件下载成功!"); } else { // 失败原因多半来自网络异常,还有文件不存在,分类名称填写异常 MessageBox.Show("文件下载失败:" + result.ToMessageShowString()); } }), result); } } /// /// 用于更新文件下载进度的方法,该方法是线程安全的 /// /// 已经接收的字节数 /// 总字节数 private void DownloadReportProgress(long receive, long totle) { if (progressBar2.InvokeRequired) { progressBar2.Invoke(new Action(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 { 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(Container_FileCountChanged), obj); return; } label1.Text = "文件数量:" + obj.ToString(); }
示例界面:
该项目的源代码公开,请遵循MIT协议,地址为:https://github.com/dathlin/FileManagment
上一篇:WPF 外发光效果
下一篇:java接口工厂模式理解