【C#】C# 自动更新(基于FTP)
目录
效果
启动软件后,会自动读取所有的 FTP 服务器文件,然后读取本地需要更新的目录,进行匹配,将 FTP 服务器的文件同步到本地
Winform 界面
一、前言
在去年,我写了一个 C# 版本的自动更新,这个是根据配置文件 + 网站文件等组成的框架,以实现本地文件的新增、替换和删除,虽然实现了自动更新的功能,但用起来过于复杂,代码量也比较大,改起来困难,后面我就想能不能弄一个 FTP 服务器进行版本的更新。平时客户端版本的更新,一般就两个需求,1.将服务器端最新的文件同步到本地,2.版本回退,如果当前版本有bug,可以随意的切换想要的版本号,这个功能在 FTP 服务器实现起来也比较简单,在 FTP 服务器里新建一个对应版本的文件夹,把对应版本的文件放进去就好了,想切换那个版本,就把 FTP 链接地址指向这个文件夹,然后同步到本地就好了,知道了这个原理,那么就来实现吧。
二、功能的实现
新建一个 winform 项目,界面如下
这几个控件分别是文件名,文件下载的进度,下载进度的百分比,具体控件名可以在源码中查看
form1 代码
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Threading.Tasks; using System.Windows.Forms; namespace update { public partial class Form1 : Form { public Form1() { InitializeComponent(); } #region 字段 /// <summary> /// 需要和FTP服务器对比的本地路径 /// </summary> private string TargetPath = string.Empty; /// <summary> /// FTP文件夹列表 /// </summary> private List<string> FTPDirectoryList = new List<string>(); /// <summary> /// FTP文件列表 /// </summary> private List<FileInfo> FTPFileList = new List<FileInfo>(); /// <summary> /// 本地文件夹列表 /// </summary> private List<string> LocalDirectorysList = new List<string>(); /// <summary> /// 本地文件列表 /// </summary> private List<FileInfo> LocalFilesList = new List<FileInfo>(); /// <summary> /// 本地文件的黑名单(不参与到更新) /// </summary> private List<string> LocalFileBlacklist = new List<string>(); /// <summary> /// ftp 和本地匹配结果,需要处理的数据 /// </summary> private UpdateResultInfo UpdateResultData = null; //读取本地文件完成 private bool ReadLocalEnd = false; //读取ftp文件完成 private bool ReadFTPEnd = false; #endregion private void Form1_Load(object sender, EventArgs e) { TargetPath = Application.StartupPath; FTPManager.DownloadProgressAction = DownProgressUpdate; //添加黑名单 AddBlacklist(); //读取配置文件 ReadConfiguration(); Start(); } private async void Start() { //刚启动就读取,会导致界面无法显示 await Task.Delay(500); //读取 FTP 所有的文件 ReadFTPFile(); //读取本地文件 ReadLocalFile(); } /// <summary> /// 添加黑名单 /// </summary> private void AddBlacklist() { LocalFileBlacklist.Add("update.exe"); LocalFileBlacklist.Add("update.exe.config"); LocalFileBlacklist.Add("update.pdb"); } /// <summary> /// 显示下载进度 /// </summary> /// <param name="fileName"></param> /// <param name="totalBytes"></param> /// <param name="totalDownloadBytes"></param> /// <param name="percent"></param> public void DownProgressUpdate(string fileName, double totalBytes, double totalDownloadBytes, int percent) { //Console.WriteLine("文件名:{0},总进度:{1},下载进度:{2},百分比:{3}", fileName, totalBytes, totalDownloadBytes, percent); FormControlExtensions.InvokeIfRequired(this, () => { Label_FileName.Text = fileName; Label_Speed.Text = string.Format("{0} / {1}", GetSize(totalBytes), GetSize(totalDownloadBytes)); Label_Percentage.Text = string.Format("{0}%", percent); ProgressBar_DownProgress.Value = percent; }); } /// <summary> /// 读取 FTP 所有的文件 /// </summary> private void ReadFTPFile() { FTPDirectoryList.Clear(); FTPFileList.Clear(); Console.WriteLine("开始读取 FTP 文件"); Task.Run(() => { Tuple<List<string>, List<FileInfo>> tuple = FTPManager.GetAllFileList(); FTPDirectoryList = tuple.Item1; FTPFileList = tuple.Item2; ReadLocalEnd = true; Console.WriteLine("读取FTP所有的文件完成"); ReadEnd(); }); } /// <summary> /// 读取本地文件 /// </summary> private void ReadLocalFile() { LocalDirectorysList.Clear(); LocalFilesList.Clear(); Console.WriteLine("开始读取本地文件"); GetDirectoryFileList(TargetPath); ReadFTPEnd = true; Console.WriteLine("读取本地文件完成"); ReadEnd(); } /// <summary> /// 获取一个文件夹下的所有文件和文件夹 /// </summary> /// <param name="path"></param> private void GetDirectoryFileList(string path) { DirectoryInfo directory = new DirectoryInfo(path); FileSystemInfo[] filesArray = directory.GetFileSystemInfos(); if (filesArray.Length == 0) return; foreach (var item in filesArray) { if (item.Attributes == FileAttributes.Directory) { //添加文件夹 //string dir = item.FullName.Replace(path, ""); LocalDirectorysList.Add(item.FullName); GetDirectoryFileList(item.FullName); } else { //文件名 string fileName = Path.GetFileName(item.FullName); //是否在黑名单中 if (!LocalFileBlacklist.Any(p => p == fileName)) { FileInfo fileType = new FileInfo(); fileType.FileName = fileName; //fileType.LastModified = File.GetLastWriteTime(item.FullName); //fileType.FileSize = new System.IO.FileInfo(item.FullName).Length; fileType.Path = item.FullName; fileType.Hash = GetHashs(item.FullName); LocalFilesList.Add(fileType); } } } } /// <summary> /// 读取配置文件 /// </summary> private void ReadConfiguration() { string ftpUrl = ConfigHelper.GetAppConfig("FtpUrl"); string ftpUser = ConfigHelper.GetAppConfig("FtpUser"); string ftpPassword = ConfigHelper.GetAppConfig("FtpPassword"); if(string.IsNullOrEmpty(ftpUrl) ) { Console.WriteLine("FTP IP地址为空"); return; } if(string.IsNullOrEmpty(ftpUser) ) { Console.WriteLine("FTP 用户名地址为空"); return; } if(string.IsNullOrEmpty(ftpPassword) ) { Console.WriteLine("FTP 用户密码地址为空"); return; } FTPManager.ftpUrl = ftpUrl; FTPManager.user = ftpUser; FTPManager.password = ftpPassword; Console.WriteLine("读取配置文件完成"); } /// <summary> /// 获取字节大小 /// </summary> /// <param name="size"></param> /// <returns></returns> private string GetSize(double size) { String[] units = new String[] { "B", "KB", "MB", "GB", "TB", "PB" }; double mod = 1024.0; int i = 0; while (size >= mod) { size /= mod; i++; } return Math.Round(size) + units[i]; } /// <summary> /// 获取文件的哈希值 /// </summary> /// <param name="path"></param> /// <returns></returns> private string GetHashs(string path) { //创建一个哈希算法对象 using (HashAlgorithm hash = HashAlgorithm.Create()) { using (FileStream file1 = new FileStream(path, FileMode.Open)) { //哈希算法根据文本得到哈希码的字节数组 byte[] hashByte1 = hash.ComputeHash(file1); //将字节数组装换为字符串 return BitConverter.ToString(hashByte1); } } } /// <summary> /// 所有的文件读取完成后 /// </summary> private void ReadEnd() { if (!ReadLocalEnd || !ReadFTPEnd) return; Console.WriteLine("所有的文件读取完成"); FormControlExtensions.InvokeIfRequired(this, () => ProgressBar_DownProgress.Visible = true ); Task.Run(() => { UpdateResultData = UpdateMatching.DetectUpdates(FTPDirectoryList, FTPFileList, LocalDirectorysList, LocalFilesList, FTPManager.ftpUrl, TargetPath); UpdateMatching.StartUpdate(UpdateResultData); Console.WriteLine("所有文件更新完成"); //FormControlExtensions.InvokeIfRequired(this, () => ProgressBar_DownProgress.Visible = true); }); } } }
软件在启动后,就会自动进行文件匹配,判断那些文件是否需要更新,但在做之前,需要先做几件事
1.本地黑名单
本地的有些文件是不必参与到更新的,比如将 update.exe 这个文件放在更新目录的,而且当前已经打开,总不能自己删除自己吧,所有有关 update.exe 相关的文件都不能参与到更新中,另一个,其他一些不需要参与到更新的文件都可以添加到黑名单中。
2.读取配置文件
ftp 的链接地址,用户名和密码,这些都是不能在代码中写死的,我一般写在配置文件中,如果你不想用户名和密码被别人看见,也比较简单,单独写一个程序集,将用户名,密码等写到一个类中,然后用我的教程中的 C# 代码混淆加密的方式把 dll 加密就行了,在 Visual Studio 2022 中反编译也是看不到的,而且,其他的反编译软件也是没用的,但是在程序运行时,用户名和密码是可以正常的读出来的。
我当前配置文件中的 用户名、密码、ftp服务器链接,主程序名 如下所示
<?xml version="1.0" encoding="utf-8" ?> <configuration> <startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" /> </startup> <appSettings> <add key="FtpUrl" value="ftp://127.0.0.1//"/> <add key="FtpUser" value="user"/> <add key="FtpPassword" value="123456"/> <add key="MainProgram" value="CNCMain"/> </appSettings> </configuration>
3.读取 FTP 文件列表
在这里,我一次性将 FTP 链接中对应目录的所有文件的 文件名,文件大小,文件哈希值,文件路径,文件夹,包含子目录文件,都读出来了,这样就没必要和之前一样,单独去搞配置文件了。
4.读取本地文件
读取本地文件是为了判断哪些文件需要替换,删除,那些文件夹需要创建,删除,总之就是让客户端这边需要更新的文件和服务器一样,没有多余的文件,也能够保持所有的文件是最新的版本。
5.匹配更新
有了 FTP 服务器对应目录的文件数据,也有了本地目录的所有文件数据,接下来就是进行匹配了,找出哪些需要创建的文件夹,需要删除的文件夹,需要更新的文件,需要删除的文件,这里匹配文件的用法依然使用哈希值匹配。
using System.Collections.Generic; internal class UpdateResultInfo { /// <summary> /// 需要创建的文件夹列表 /// </summary> public List<string> CreateFolderList { get; set; } = new List<string>(); /// <summary> /// 需要删除的文件夹列表 /// </summary> public List<string> DeleteFolderList { get; set; } = new List<string>(); /// <summary> /// 本地需要更新的文件列表 /// </summary> public List<DownloadFileInfo> LocalUpdateFileList { get;set; } = new List<DownloadFileInfo>(); /// <summary> /// 本地需要删除的文件列表 /// </summary> public List<FileInfo> LocalDeleteFileList { get; set; } = new List<FileInfo>(); }
这里会单独写一个方法来得出想要的结果,然后由单独的方法去处理这些结果。
下面是控制台效果,不喜欢也可以去掉,由于本地只有 update.exe 文件,而 update.exe 又在黑名单中,所以默认会把 ftp 服务器对应目录的所有文件下载下来,如果服务器文件和客户端文件是一样的,那么这个文件是不会下载的,这个我经过测试,是没问题的。
界面效果
6.版本的切换
版本的切换也比较简单,在配置文件中,改对应的链接就好了,客户端就会自动和服务器对应的版本进行对比了。
比如:
ftp://127.0.0.1//v1.0.1
ftp://127.0.0.1//v1.0.2
ftp://127.0.0.1//v1.0.3
那么关于 FTP 自动更新的流程就是这个样子了,上面的功能,都是经过各种测试,花了一些时间写出来的,流程是可以走的通的,有兴趣的朋友也可以自己写写看,感觉 FTP 版,要比我之前写的 HTTP 版的要简单很多。
代码我并没有全部贴出来,有需要的可以去支持一下我,在此谢谢了,有源码有疑问的可以私信我,我看到后会回复的。
源码地址:点击下载
三、环境搭建
搭建 IIS 版 FTP 服务器
参考帖子:
【Windows】之搭建 FTP 服务器_windows搭建ftp服务器-CSDN博客
虽然框架是可用的,但还是要注意以下几点:
1.FTP服务器搭建完成后,如果你的电脑IP地址变了,记得更改,否则客户端会访问不了。
2.FTP 文件夹名字,尽量不要用空格,因为在访问的时候,是一个链接形式进行访问的,链接中有空格可能会导致无法访问。
查看 FTP 的链接地址:
防火墙
在使用之前,FTP服务端电脑记得关闭防火墙,一定要保证客户端的电脑能 ping 的通
FTP 登陆测试
为了检测 FTP 是否能连接的上,最好先在文件管理器和网页进行测试,下面我都演示下。
1)使用浏览器,在浏览器输入:ftp://192.168.30.83/(你自己的FTP服务器地址),注意 ip 地址后面使用的是单斜杠
然后回车,就会弹框,让你输入用户名和密码
这时候,输入你创建 FTP用的 windows 账号就行了。
输入完成,就能看到你的 FTP 服务器文件了
2)使用文件管理器
使用文件管理器操作差不多,输入地址,按回车就行了
输入账号和密码
打开了 FTP 的根目录,说明 FTP 服务器正常使用。
四、常见问题
问题1:界面不动
如果界面一直停留在这个界面不动,一般情况是配置文件中的 FTP 地址,账号和密码配置出了问题,这时候,首先检查配置文件的数据是否正确。
如果还是无法读取 FTP 服务器的文件,那就先在浏览器中查看 FTP 服务器是否能连接上,如果 FTP 服务器中能连接,那么就把源码复制到客户端中,用 Visual Studio 2022 进行打开断点查看,一般来说,在浏览器中能查看 FTP 服务器,当前软件也可以连接的上。
2023.12.30 更新
针对当前的源码进行大量优化,两个项目对比:
除优化外,同时删除了一部功能,改动如下:
1.项目从 Winform 改成了控制台应用
2.配置文件从 App.config 里读取,改为读取自定义的配置文件 ftpAccount.config
3.修复了本地黑名单文件和黑名单文件夹内文件重复下载问题
4.匹配文件算法重写
5.去掉了删除本地多余的文件夹和文件功能(除了要更新的文件,其他文件和文件夹都不会被删除)
6.将黑名单文件和黑名单文件夹的读取放入到了本地的 txt 文件中,如下:
黑名单和黑名单文件夹一样,文件夹直接写文件夹名字,文件的话要加后缀,比如 xxx.exe,用换行作为区分
当前的源码如果遇到 bug 或者有更好的建议,欢迎私信或者评论,我会改正过来,然后更新资源文件,谢谢。
源码地址:点击下载
结束
end
猜你喜欢
- 【C#】C# Winform 自定义进度条ProgressBar
- 效果:一、前言Winfrom各种老毛病真的不适合做大型项目,甚至中型项目都不适合,一些小功能都能把你折腾半死,比如,我想在界面上显示一个进度条,用来显示现在硬盘和内存已经使用了多少,使用了 ProgressBar 控件你看看效果:进度条中间一直有个白色光影在晃来晃去的,是不是想让别人感慨:“哇!好强的光芒,我的眼睛快睁不开了...”。而且背景颜色无法改变,这个动画也无法关掉,为了解决这两个问题,我找了很久,终于找到了下面的解决方法。二、自定义进度条于是我在网上找了一些资料,有到效果有,但不是特别
- 【C#】Winform NanUI 0.77版本 读取本地资源(扩展功能)
- 一、前言在NanUI官方的文档中,原本是有一个NanUI.FileResourceHandler的扩展包的,但现在官方已经无法下载了,现在只有0.88版本中有一个NanUI.LocalFileResource程序包,而0.77版本只剩下了一个读取嵌入式资源的程序包。关于NanUI:NanUI | .Net/.Net Core界面组件NanUI 0.7版正式发布 - 林选臣 - 博客园在扩展功能之前,请参考[资源处理器]-04 自定义资源处理器 - 知乎 ,我参考这个帖子进行扩展的,也不
- 【C#】C# Winfrom 常用功能整合-1
- 目录Winform 最大化遮挡任务栏和全屏显示问题Winfrom 给图片画 矩形,椭圆形,文字Winfrom TabControl选项卡 动态添加,删除,修改Winform ErrorProvider控件Winform 读取Resources图片Winfrom 读取内存条占用大小,硬盘占用大小Winform 全局捕获异常Winform 用线程写入TXT文件,并更新UI和进度Winform 摄像头识别二维码,保存图片Winform 判断窗体是否已打开Winform 动态添加菜单列表,点击切换对应面
- 【C#】C# Winform 配置文件App.config
- 目录一、简介二、添加引用 三、添加节点1.普通配置节点2.数据源配置节点四、管理类 ConfigHelper.cs1.获取配置节点2.更新或加入配置节点结束一、简介在C#中,配置文件很常用,ASP.NET 和 Winform 名称不同,用法一样,如下图config 文件通常用来存储一些需要修改的数据,比如用户名密码,连接数据库的IP地址等,而不是在代码中写死。有人可能会问,那我自己自定义一个配置文件也行,为什么要用它这个?区别当然有,微软自己封装的读取和写入会更简单一些,你自己封装的,
- 【C#】CSDK/IDE-VSCode 搭建 C# 开发环境
- 最近准备写 C# 的笔记总结专栏 bug 笔记本硬盘空间实在是不够用了 根本没有办法再安装一个 Visual Studio 集成开发环境了!!! 在学 Java 的过程中基本都是用记事本和命令提示符……再也不想经历了 &nbs
- 【C#】C# Winform 日志系统
- 目录一、效果1.刷新日志效果2.单独日志的分类3.保存日志的样式二、概述三、日志系统API1.字段Debug.IsScrollingDebug.VersionDebug.LogMaxLenDebug.LogTitleDebug.IsConsoleShowLog2.方法Debug.Log(string)Debug.Log(string, params object[])Debug.Logs(string)Debug.Logs(string, params object[])Debug.LogSav
- 【C#】Winform NanUI 0.77版本 JS和C#相互调用
- 目录一、导入插件二、常用方法三、C#和JS相互调用1.C# 调用JS2.JS调用C#方法3.完整版C#代码4.完整版JS代码结束一、导入插件用的NanUI版本0.77参考官方地址:https://docs.formium.net/zh-hans/tutorial/first-app.html二、常用方法基础代码:using NetDimension.NanUI; using NetDimension.NanUI.Browser; class MainW
- 【C#】Winform NanUI 0.88版本 用官方源码搭建原生态开发环境
- 目录一、需求二、搭建原生开发环境1.导入源码2.解决源码报错错误1错误23.导入其他项目4.官方Demo运行效果三、创建自己的NanUI项目1.新建项目2.导入NanUI.Runtime扩展包3.添加NanUI程序集的引用4.MainIndex主界面相关代码5.Program程序入口相关代码6.读取本地前端文件的处理四、测试项目效果五、结束一、需求NanUI 插件确实很方便,但想改其中的需求怎么办,下面就来自己搭建NanUI 原生开发环境,在此很感谢作者免费的开源。官方源码地址:GitHub -