您的当前位置:首页>全部文章>文章详情

【C#】C# 自动更新(基于FTP)

CrazyPanda发表于:2024-02-03 22:38:07浏览:366次TAG:

目录

一、前言

二、功能的实现

1.本地黑名单

2.读取配置文件

3.读取 FTP 文件列表

4.读取本地文件

5.匹配更新

6.版本的切换

三、环境搭建

四、常见问题

2023.12.30 更新

结束




效果

启动软件后,会自动读取所有的 FTP 服务器文件,然后读取本地需要更新的目录,进行匹配,将 FTP 服务器的文件同步到本地

01174706_63da351a96fff24847.gif

Winform 界面

01174706_63da351a96fff24847.gif


一、前言

在去年,我写了一个 C# 版本的自动更新,这个是根据配置文件 + 网站文件等组成的框架,以实现本地文件的新增、替换和删除,虽然实现了自动更新的功能,但用起来过于复杂,代码量也比较大,改起来困难,后面我就想能不能弄一个 FTP 服务器进行版本的更新。平时客户端版本的更新,一般就两个需求,1.将服务器端最新的文件同步到本地,2.版本回退,如果当前版本有bug,可以随意的切换想要的版本号,这个功能在 FTP 服务器实现起来也比较简单,在 FTP 服务器里新建一个对应版本的文件夹,把对应版本的文件放进去就好了,想切换那个版本,就把 FTP 链接地址指向这个文件夹,然后同步到本地就好了,知道了这个原理,那么就来实现吧。


二、功能的实现

新建一个 winform 项目,界面如下

1.png

这几个控件分别是文件名,文件下载的进度,下载进度的百分比,具体控件名可以在源码中查看

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 服务器对应目录的所有文件下载下来,如果服务器文件和客户端文件是一样的,那么这个文件是不会下载的,这个我经过测试,是没问题的。

01174706_63da351a96fff24847.gif

界面效果

01174706_63da351a96fff24847.gif


6.版本的切换

版本的切换也比较简单,在配置文件中,改对应的链接就好了,客户端就会自动和服务器对应的版本进行对比了。

1.png

比如:

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 的链接地址:

1.png

1.png


防火墙

在使用之前,FTP服务端电脑记得关闭防火墙,一定要保证客户端的电脑能 ping 的通

1.png


FTP 登陆测试

为了检测 FTP 是否能连接的上,最好先在文件管理器和网页进行测试,下面我都演示下。

1)使用浏览器,在浏览器输入:ftp://192.168.30.83/(你自己的FTP服务器地址),注意 ip 地址后面使用的是单斜杠1.png

然后回车,就会弹框,让你输入用户名和密码

1.png

这时候,输入你创建 FTP用的 windows 账号就行了。

输入完成,就能看到你的 FTP 服务器文件了

1.png

2)使用文件管理器

使用文件管理器操作差不多,输入地址,按回车就行了

1.png

输入账号和密码

1.png

1.png

打开了 FTP 的根目录,说明 FTP 服务器正常使用。


四、常见问题

问题1:界面不动

如果界面一直停留在这个界面不动,一般情况是配置文件中的 FTP 地址,账号和密码配置出了问题,这时候,首先检查配置文件的数据是否正确。

1.png

如果还是无法读取 FTP 服务器的文件,那就先在浏览器中查看 FTP 服务器是否能连接上,如果 FTP 服务器中能连接,那么就把源码复制到客户端中,用 Visual Studio 2022 进行打开断点查看,一般来说,在浏览器中能查看 FTP 服务器,当前软件也可以连接的上。


2023.12.30 更新

针对当前的源码进行大量优化,两个项目对比:

1.png

除优化外,同时删除了一部功能,改动如下:

1.项目从 Winform 改成了控制台应用

2.配置文件从 App.config 里读取,改为读取自定义的配置文件 ftpAccount.config

3.修复了本地黑名单文件和黑名单文件夹内文件重复下载问题

4.匹配文件算法重写

5.去掉了删除本地多余的文件夹和文件功能(除了要更新的文件,其他文件和文件夹都不会被删除)

6.将黑名单文件和黑名单文件夹的读取放入到了本地的 txt 文件中,如下:

1.png

黑名单和黑名单文件夹一样,文件夹直接写文件夹名字,文件的话要加后缀,比如 xxx.exe,用换行作为区分

1.png

当前的源码如果遇到 bug 或者有更好的建议,欢迎私信或者评论,我会改正过来,然后更新资源文件,谢谢。

源码地址:点击下载



结束

end

原文链接C# 自动更新(基于FTP)_c# 程序自动升级-CSDN博客

猜你喜欢

【C#】C# Winform 三层架构
一、介绍三层架构是 C# 桌面开发中比较常用的框架,是由&nbsp;表示层(UI)、业务逻辑层(BLL)和数据访问层(DAL)三层架构组成,目的是为了 “高内聚,低耦合”。开发人员分工更明确,将精力更专注于应用系统核心业务逻辑的分析、设计和开发,加快项目的进度,提高了开发效率,有利于项目的更新和维护工作。从三层架构可以看到,很类似于&nbsp;Web 前端开发的 MVC 框架(视图View,模型Model,控制Contorller),但本质上也有不同的地方,比如都有视图(三层中叫 UI),Mod
发表于:2024-02-03 浏览:295 TAG:
【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
发表于:2024-02-18 浏览:367 TAG:
【C#】C# Winform GDI+ 绘图
目录一、概述二、绘图1.画直线2.画矩形3.画圆、圆弧4.画扇形5.画多边形6.绘制字符串7.填充图形结束一、概述Graphics类是GDI+技术的一个基本类。GDI+(Graphics Device Interface)是.NET框架的重要组成部分,提供对二维图形图像和文字排版处理的支持。GDI+相关的类分布在下列命名空间中: System.Drawing:提供了最基本的绘图功能(比如画直线、矩形、椭圆等); System.Drawing.Drawing2D: 提供了高级的二维和矢量绘图功能(
发表于:2024-02-18 浏览:387 TAG:
【C#】C# Winfrom Chart 图表控件 柱状图、折线图
目录一、参考二、柱状图1.新建项目2.修改图表样式3.绑定数据4.删除Series1图例1)使用属性窗体删除2)使用代码删除5.自定义X轴的刻度值1)使用List绑定2)使用LabelStyle.Format绑定6.自定义Y轴的刻度值7.X轴刻度值显示不全的解决方法8.修改X Y刻度值的字体样式9.X轴刻度值旋转90°10.禁用Y轴刻度线11.去掉Y轴刻度值12.改变柱子的宽度13.设置网格线的颜色14.设置网格线的线型三、折线图1.图表设置2.绑定数据结束效果:一、参考c# Chart设置样式
发表于:2024-02-02 浏览:425 TAG:
【C#】C# Winform 相册功能,图片缩放,拖拽,预览图分页
一、前言在一些项目中也会用到预览图片的功能,至于为什么有一个添加图片的按钮,是因为有些项目,比如视觉相关的项目,摄像头拍摄图片,然后显示在界面上,拍一次显示一张。另一个,就是分页功能,当预览图位置不够用时就会用到。当前软件的功能1.添加图片如果8个预览图都满了,会自动分页,就可以点击上一页,或者下一页了。2.点击预览图显示大图点击预览图,之前的拖拽和放大会自动复位3.大图可以拖拽,放大,缩小如果图片比较小,有这个功能就看到图片的更多细节了。4.图片倒序排列最后拍摄的图片,始终显示在前面,方便用户
发表于:2024-02-02 浏览:324 TAG:
【C#】C# NLua Winform 热更新
一、概述NLua 是一个用于 .NET 平台的 Lua 脚本绑定库。它允许在 C# 代码中嵌入 Lua 脚本,并允许两者之间进行交互。NLua 的主要特点包括:轻量级:NLua 是一个轻量级的库,易于集成到现有的 .NET 项目中。动态类型:Lua 是动态类型的语言,这意味着变量的类型可以在运行时改变。灵活的绑定:NLua 提供了灵活的绑定机制,使得 C# 和 Lua 之间的数据交互变得简单。丰富的 API:NLua 提供了丰富的 API,以便在 Lua 脚本中调用 .NET 的类和方法。调试支
发表于:2024-02-18 浏览:358 TAG:
【C#】C# Winform 文本面板带滚动条
在PC软件开发中经常有这样的需求,需要在一个固定大小的面板中显示一些内容,并且面板能上下拖动,将所有的内容完整的展示,有点类似网页上看新闻,如果要在 winfrom 中要如何实现的呢,下面就演示如何实现的吧效果:1.新建一个winform 项目,在界面中拖入一个Panel&nbsp;将 panel1 的&nbsp;AutoScroll 设置为 True2.再次拖入一个&nbsp;Panel ,将高度拉长,这时就自动出现了滚动条,只是此时里面还没有任何内容,下面就在 panel2 中加入一点内容。
发表于:2024-02-03 浏览:330 TAG:
【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&nbsp;NetDimension.NanUI; using&nbsp;NetDimension.NanUI.Browser; &nbsp; class&nbsp;MainW
发表于:2024-02-06 浏览:392 TAG:
【C#】Winform NanUI 0.77版本 清除Cookie等本地缓存
目录需求NanUI0.88版本的缓存路径NanUI0.77版本的缓存路径试着实现功能添加扩展出现的问题解决问题结束需求清除web前端保存的一些的数据,或者清除浏览器缓存会用到。NanUI0.88版本的缓存路径在NanUI0.88版本中,只要在Program.cs启动模块中调用app.ClearCacheFile();就可以清除浏览的缓存了。0.88版本的缓存路径:&quot;C:\\Users\\Administrator\\AppData\\Roaming\\Net Dimension Stu
发表于:2024-02-08 浏览:316 TAG:
【C#】C# Winform 定时清理日志
一、前言在 Winform 开发中经常有这样的需求,在用户执行一些操作不正确时,需要将错误信息反馈给用户,比如:登录密码不正确,无法连接到服务器等,一般常见的用法有两个:1.弹框使用&nbsp;MessageBox.Show(&quot;密码错误&quot;); 这样的方式,弹框后,用户必须点击确定后才能执行下一步操作,给用户的体验并不是特别好。2.在界面中显示错误信息,定时清除如果是输入框,直接用&nbsp;ErrorProvider 控件就行了。如果只是做一些简单的提示信息,那么就要定时清除
发表于:2024-01-31 浏览:307 TAG: