桌面應用程式部署後,如何讓使用者無感取得最新版本是關鍵課題。本文設計一套基於 Web Service 的自動更新架構:版本比對 → 差異下載 → 備份舊版 → 原子替換 → 重啟應用,並處理網路異常、權限不足、檔案鎖定等實務問題。
┌─────────────┐ HTTP/HTTPS ┌──────────────┐
│ Client App │ ◄─────────────────► │ Update Server │
│ │ 1. Get Version XML │ (Web API) │
│ - Version │ 2. Download Files │ │
│ - Config │ 3. Apply Update │ - Version.xml│
└─────────────┘ │ - Files.zip │
│ └──────────────┘
▼
┌─────────────────────┐
│ Local Update Logic │
│ - Temp Folder │
│ - Backup & Replace │
│ - Restart Self │
└─────────────────────┘
public class ModuleVersion
{
public string Name { get; set; } // 模組名稱
public string Version { get; set; } // 版本號(如 1.2.3.4)
public string InternetUrl { get; set; } // 外網下載 URL
public string IntranetUrl { get; set; } // 內網下載 URL
public string Hash { get; set; } // SHA256 校驗碼
}
<?xml version="1.0" encoding="utf-8"?>
<Modules>
<Module>
<Name>CoreLib</Name>
<Version>2.1.0.0</Version>
<InternetUrl>https://cdn.example.com/update/CoreLib_2.1.0.0.zip</InternetUrl>
<IntranetUrl>https://192.168.1.100/update/CoreLib_2.1.0.0.zip</IntranetUrl>
<Hash>A3F2...</Hash>
</Module>
<Module>
<Name>MainApp</Name>
<Version>3.0.5.1</Version>
<InternetUrl>https://cdn.example.com/update/MainApp_3.0.5.1.zip</InternetUrl>
<IntranetUrl>https://192.168.1.100/update/MainApp_3.0.5.1.zip</IntranetUrl>
<Hash>F7B9...</Hash>
</Module>
</Modules>
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text.Json;
using System.Threading.Tasks;
using System.Xml.Linq;
public class AutoUpdater
{
private readonly HttpClient _httpClient;
private readonly string _appDirectory;
private readonly string _tempDirectory;
private readonly string _configPath;
private readonly bool _useInternet;
public AutoUpdater(string appDirectory, bool useInternet = true)
{
_appDirectory = appDirectory;
_useInternet = useInternet;
_tempDirectory = Path.Combine(_appDirectory, "temp_update");
_configPath = Path.Combine(_appDirectory, "UpdateConfig.json");
_httpClient = new HttpClient { Timeout = TimeSpan.FromMinutes(10) };
}
public async Task CheckAndUpdateAsync(IProgress progress = null)
{
progress?.Report("連線更新伺服器...");
var serverModules = await FetchServerVersionAsync();
if (serverModules == null) return UpdateResult.Fail("無法取得伺服器版本資訊");
progress?.Report("比對本地版本...");
var localModules = LoadLocalConfig();
var updates = CompareVersions(localModules, serverModules);
if (!updates.Any()) return UpdateResult.Success("已是最新版本");
progress?.Report($"發現 {updates.Count} 個模組需更新,開始下載...");
foreach (var mod in updates)
{
string url = _useInternet ? mod.InternetUrl : mod.IntranetUrl;
string zipPath = Path.Combine(_tempDirectory, $"{mod.Name}.zip");
await DownloadWithProgressAsync(url, zipPath, p =>
progress?.Report($"下載 {mod.Name}:{p:P0}"));
if (!VerifyHash(zipPath, mod.Hash))
return UpdateResult.Fail($"{mod.Name} 檔案校驗失敗,可能遭竄改");
}
progress?.Report("備份舊版本並套用更新...");
ApplyUpdates(updates);
progress?.Report("更新完成,準備重啟...");
SaveConfig(serverModules);
RequestRestart();
return UpdateResult.Success("更新成功");
}
// --- 私有輔助方法 ---
private async Task> FetchServerVersionAsync()
{
try
{
string xml = await _httpClient.GetStringAsync("https://api.example.com/version.xml");
var doc = XDocument.Parse(xml);
return doc.Descendants("Module").Select(m => new ModuleVersion
{
Name = m.Element("Name")?.Value,
Version = m.Element("Version")?.Value,
InternetUrl = m.Element("InternetUrl")?.Value,
IntranetUrl = m.Element("IntranetUrl")?.Value,
Hash = m.Element("Hash")?.Value
}).ToList();
}
catch { return null; }
}
private Dictionary LoadLocalConfig()
{
if (!File.Exists(_configPath)) return new Dictionary();
try
{
string json = File.ReadAllText(_configPath);
return JsonSerializer.Deserialize>(json)
?? new Dictionary();
}
catch { return new Dictionary(); }
}
private List CompareVersions(Dictionary local, List server)
{
var updates = new List();
foreach (var srv in server)
{
if (!local.TryGetValue(srv.Name, out var loc) ||
Version.Parse(loc.Version) < Version.Parse(srv.Version))
{
updates.Add(srv);
}
}
return updates;
}
private async Task DownloadWithProgressAsync(string url, string destPath, Action progress)
{
Directory.CreateDirectory(Path.GetDirectoryName(destPath));
using var response = await _httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
response.EnsureSuccessStatusCode();
long? total = response.Content.Headers.ContentLength;
using var stream = await response.Content.ReadAsStreamAsync();
using var fs = File.Create(destPath);
byte[] buffer = new byte[8192];
long downloaded = 0;
int read;
while ((read = await stream.ReadAsync(buffer)) > 0)
{
await fs.WriteAsync(buffer.AsMemory(0, read));
downloaded += read;
if (total.HasValue) progress?.Invoke((double)downloaded / total.Value);
}
}
private bool VerifyHash(string filePath, string expectedHash)
{
using var sha256 = SHA256.Create();
using var fs = File.OpenRead(filePath);
byte[] hash = sha256.ComputeHash(fs);
string actual = Convert.ToHexString(hash);
return string.Equals(actual, expectedHash, StringComparison.OrdinalIgnoreCase);
}
private void ApplyUpdates(List updates)
{
Directory.CreateDirectory(_tempDirectory);
foreach (var mod in updates)
{
string zipPath = Path.Combine(_tempDirectory, $"{mod.Name}.zip");
string extractPath = Path.Combine(_tempDirectory, mod.Name);
// 解壓縮
ZipFile.ExtractToDirectory(zipPath, extractPath, true);
// 備份並替換
foreach (string file in Directory.GetFiles(extractPath, "*.*", SearchOption.AllDirectories))
{
string relPath = file.Substring(extractPath.Length + 1);
string destFile = Path.Combine(_appDirectory, relPath);
Directory.CreateDirectory(Path.GetDirectoryName(destFile));
if (File.Exists(destFile))
File.Move(destFile, destFile + $".bak_{DateTime.Now:yyyyMMddHHmmss}", true);
File.Move(file, destFile);
}
}
}
private void SaveConfig(List modules)
{
var dict = modules.ToDictionary(m => m.Name, m => m);
string json = JsonSerializer.Serialize(dict, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(_configPath, json);
}
private void RequestRestart()
{
string exePath = System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName;
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
{
FileName = exePath,
UseShellExecute = true
});
Environment.Exit(0);
}
}
public record UpdateResult(bool Success, string Message)
{
public static UpdateResult Success(string msg) => new(true, msg);
public static UpdateResult Fail(string msg) => new(false, msg);
}
Version.Parse 進行語意版本比較,正確處理 1.10 > 1.2 等情況。temp_update,驗證通過後一次性移動替換。MoveFileEx + MOVEFILE_DELAY_UNTIL_REBOOT)。%LocalAppData%\YourApp(使用者目錄),或採用 ClickOnce / MSIX 部署自動處理權限。HttpClientHandler.UseProxy = true),提供內網鏡像站選項(IntranetUrl)。backup_日期,啟動時檢測上次更新是否完成,未完成自動還原備份。bsdiff),客戶端下載差異包並套用(bspatch),大幅減少頻寬。成熟方案參考 Squirrel.Windows、NetSparkle。自動更新系統涉及網路傳輸、檔案系統操作、安全驗證、程序生命週期管理等多個面向。本文提供的核心架構涵蓋版本比對、安全下載、校驗、原子替換與重啟的完整流程。實務專案中,強烈建議採用成熟開源框架(Squirrel/NetSparkle),專注於業務邏輯而非底層更新機制的維護。