效果
启动软件后,会自动读取所有的 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 字段
///
/// 需要和FTP服务器对比的本地路径
///
private string TargetPath = string.Empty;
///
/// FTP文件夹列表
///
private List FTPDirectoryList = new List();
///
/// FTP文件列表
///
private List FTPFileList = new List();
///
/// 本地文件夹列表
///
private List LocalDirectorysList = new List();
///
/// 本地文件列表
///
private List LocalFilesList = new List();
///
/// 本地文件的黑名单(不参与到更新)
///
private List LocalFileBlacklist = new List();
///
/// ftp 和本地匹配结果,需要处理的数据
///
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();
}
///
/// 添加黑名单
///
private void AddBlacklist()
{
LocalFileBlacklist.Add("update.exe");
LocalFileBlacklist.Add("update.exe.config");
LocalFileBlacklist.Add("update.pdb");
}
///
/// 显示下载进度
///
///
///
///
///
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;
});
}
///
/// 读取 FTP 所有的文件
///
private void ReadFTPFile()
{
FTPDirectoryList.Clear();
FTPFileList.Clear();
Console.WriteLine("开始读取 FTP 文件");
Task.Run(() =>
{
Tuple, List> tuple = FTPManager.GetAllFileList();
FTPDirectoryList = tuple.Item1;
FTPFileList = tuple.Item2;
ReadLocalEnd = true;
Console.WriteLine("读取FTP所有的文件完成");
ReadEnd();
});
}
///
/// 读取本地文件
///
private void ReadLocalFile()
{
LocalDirectorysList.Clear();
LocalFilesList.Clear();
Console.WriteLine("开始读取本地文件");
GetDirectoryFileList(TargetPath);
ReadFTPEnd = true;
Console.WriteLine("读取本地文件完成");
ReadEnd();
}
///
/// 获取一个文件夹下的所有文件和文件夹
///
///
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);
}
}
}
}
///
/// 读取配置文件
///
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("读取配置文件完成");
}
///
/// 获取字节大小
///
///
///
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];
}
///
/// 获取文件的哈希值
///
///
///
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);
}
}
}
///
/// 所有的文件读取完成后
///
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 中也是看不到的,而且,反编译也是没用的,但是在程序运行时,是能正常的读出来的。
3.读取 FTP 文件列表
在这里,我一次性将 FTP 链接中对应目录的所有文件的 文件名,文件大小,文件哈希值,文件路径,文件夹,包含子目录文件,都读出来了,这样就没必要 和之前一样,单独去搞配置文件了。
4.读取本地文件
读取本地文件是为了判断那些文件需要替换,删除,那些文件夹需要创建,删除,总之就是让客户端这边和服务器一样,没有多余的文件,也能够保持文件的一致性。
5.匹配更新
有了 FTP 服务器对应目录的文件数据,也有了本地目录的所有文件数据,接下来就是进行匹配了,找出那些 需要创建的文件夹,需要删除的文件夹,需要更新的文件,需要删除的文件,这里匹配文件的用法依然使用哈希值匹配。
using System.Collections.Generic;
internal class UpdateResultInfo
{
///
/// 需要创建的文件夹列表
///
public List CreateFolderList { get; set; } = new List();
///
/// 需要删除的文件夹列表
///
public List DeleteFolderList { get; set; } = new List();
///
/// 本地需要更新的文件列表
///
public List LocalUpdateFileList { get;set; } = new List();
///
/// 本地需要删除的文件列表
///
public List LocalDeleteFileList { get; set; } = new List();
}
这里会单独写一个方法来得出想要的结果,然后由单独的方法去处理这些结果。
下面是控制台效果,不喜欢也可以去掉,下面是本地没有需要更新的文件,这会把 ftp 服务器对应目录的所有文件下载下来
界面效果
6.版本的切换
版本的切换也比较简单,在 ftp 链接中改对应的目录就行了
比如:
ftp://127.0.0.1//v1.0.1
ftp://127.0.0.1//v1.0.2
ftp://127.0.0.1//v1.0.3
代码我并没有全部贴出来,有需要的可以去支持一下我,在此谢谢了,有源码有疑问的可以私信我,我看到后会回复的。
源码地址:点击下载
end