using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Deployment.Application; using System.Diagnostics; using System.Drawing; using Newtonsoft.Json; using System.IO; using System.Linq; using System.Net; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using System.Net.Http; using static System.Windows.Forms.VisualStyles.VisualStyleElement; using System.Reflection.Emit; namespace ProgramLauncher { public partial class Form1 : Form { static string downloadServerUrl = "http://xr.flexing.ai:3030/XR_Server"; static string downloadFolderName = "Build"; static string jsonFileName = "file_list.json"; static string localVersionID = "versionID.txt"; long totalBytesToDownload = 0; long totalBytesDownloaded = 0; static readonly HttpClient httpClient = new HttpClient(); public Form1() { downloadServerUrl = CombineUrl(downloadServerUrl, Program.projectName); InitializeComponent(); } private async void Form1_Load(object sender, EventArgs e) { Label_UpdateFileName.Text = string.Empty; await CheckUpdate(); } private void WriteText(string text) { Label_UpdateFileName.Text = text; } private async Task CheckUpdate() { var serverFileList = await GetServerFileList(downloadServerUrl); if (serverFileList == null) return; string latestVersionID = serverFileList.VersionID; string localVersionID = GetLocalVersion(); if (latestVersionID != localVersionID) { // ✅ [1] 전체 크기 계산 foreach (var file in serverFileList.Files) { string fileUrl = CombineUrl(downloadServerUrl, Path.Combine(downloadFolderName, file.Key)); using (var response = await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, fileUrl))) { if (response.IsSuccessStatusCode && response.Content.Headers.ContentLength.HasValue) { totalBytesToDownload += response.Content.Headers.ContentLength.Value; } } } // ✅ [2] ProgressBar 초기화 ProgressBar.Invoke((MethodInvoker)(() => { ProgressBar.Minimum = 0; ProgressBar.Maximum = (int)Math.Max(totalBytesToDownload, 1); // 0 방지 ProgressBar.Value = 0; })); foreach (var file in serverFileList.Files) { string localFilePath = Path.Combine(downloadFolderName, file.Key); string serverHash = file.Value; if (!File.Exists(localFilePath) || GetFileHash(localFilePath) != serverHash) { Label_UpdateFileName.Text = localFilePath; string downloadFolder = CombineUrl(downloadServerUrl, downloadFolderName); string downloadFile = Path.Combine(downloadFolder, file.Key); await DownloadFile(downloadFile, localFilePath, (downloadedBytes) => { totalBytesDownloaded += downloadedBytes; ProgressBar.Invoke((MethodInvoker)(() => { ProgressBar.Value = (int)Math.Min(totalBytesDownloaded, ProgressBar.Maximum); })); }); } } var serverFilePaths = new HashSet( serverFileList.Files.Keys.Select(file => file.Replace('\\', '/')) // 서버의 파일 경로를 통일 ); foreach (string localFile in Directory.GetFiles(downloadFolderName, "*", SearchOption.AllDirectories)) { // 현재 파일의 상대 경로를 구함 (localFolder 기준) string relativePath = localFile.Substring(downloadFolderName.Length + 1).Replace('\\', '/'); if (!serverFilePaths.Contains(relativePath)) { File.Delete(localFile); } } SaveLocalVersion(latestVersionID); } StartExecutableFile(downloadFolderName); this.Close(); } private async Task GetServerFileList(string serverUrl) { using (WebClient client = new WebClient()) { try { string serverJsonUrl = CombineUrl(serverUrl, jsonFileName); string json = await client.DownloadStringTaskAsync(serverJsonUrl); return JsonConvert.DeserializeObject(json); } catch (Exception ex) { return null; } } } private string GetLocalVersion() { return File.Exists(localVersionID) ? File.ReadAllText(localVersionID).Trim() : ""; } private void SaveLocalVersion(string version) { File.WriteAllText(localVersionID, version); } private string GetFileHash(string filePath) { using (var sha256 = SHA256.Create()) using (var stream = File.OpenRead(filePath)) { byte[] hash = sha256.ComputeHash(stream); return BitConverter.ToString(hash).Replace("-", "").ToLower(); } } private async Task DownloadFile(string url, string outputPath, Action reportProgress) { try { Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? ""); using (var response = await httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead)) { response.EnsureSuccessStatusCode(); using (var stream = await response.Content.ReadAsStreamAsync()) using (var fileStream = new FileStream(outputPath, FileMode.Create, FileAccess.Write, FileShare.None)) { byte[] buffer = new byte[81920]; int bytesRead; while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length)) > 0) { await fileStream.WriteAsync(buffer, 0, bytesRead); reportProgress(bytesRead); // 매번 다운로드된 바이트 보고 } } } } catch (Exception ex) { WriteText($"❌ 다운로드 실패: {ex.Message}"); } } private void StartExecutableFile(string executeFolder) { string[] exeFiles = Directory.GetFiles(executeFolder, "*.exe", SearchOption.TopDirectoryOnly); if (exeFiles.Length == 0) { return; } string executeFilePath = exeFiles.FirstOrDefault(file => !file.EndsWith("UnityCrashHandler64.exe", StringComparison.OrdinalIgnoreCase)); string executeFileName = Path.GetFileName(executeFilePath); Process process = new Process(); process.StartInfo.UseShellExecute = true; process.StartInfo.WorkingDirectory = executeFolder; process.StartInfo.FileName = executeFileName; process.Start(); } private string CombineUrl(string baseUrl, string relativePath) { return $"{baseUrl.TrimEnd('/')}/{relativePath.TrimStart('/')}"; } } class ServerFileList { public string VersionID { get; set; } = ""; public Dictionary Files { get; set; } = new Dictionary(); } }