diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 997554ce..9260d1c1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,7 +1,7 @@ name: build on: [push, pull_request] jobs: - dotnet-build: + dotnet-build-logic: if: true runs-on: windows-latest steps: @@ -14,11 +14,41 @@ jobs: - name: Build Logic run: dotnet build "./logic/logic.sln" -c Release + dotnet-build-install: + if: true + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - name: Setup .NET Core + uses: actions/setup-dotnet@v3 + with: + dotnet-version: 7.0.x + - name: Build Installer - run: dotnet build "./installer/installer.sln" -c Release + run: dotnet build "./installer/installer.sln" -c Release -f net7.0-windows10.0.19041.0 + dotnet-build-launcher: + if: true + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - name: Setup .NET Core + uses: actions/setup-dotnet@v3 + with: + dotnet-version: 6.0.x + - name: Build Launcher run: dotnet build "./launcher/launcher.sln" -c Release - + + dotnet-build-playback: + if: true + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - name: Setup .NET Core + uses: actions/setup-dotnet@v3 + with: + dotnet-version: 6.0.x + - name: Build Playback run: dotnet build "./playback/playback.sln" -c Release diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index c7eeb3a0..33f924e5 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -13,7 +13,7 @@ jobs: exclude: './players' inplace: False - dotnet-format-checking: + dotnet-format-checking-logic: runs-on: windows-latest steps: - uses: actions/checkout@v4 @@ -26,17 +26,44 @@ jobs: run: | dotnet restore "./logic/logic.sln" dotnet format "./logic/logic.sln" --severity error --no-restore --verify-no-changes - + + dotnet-format-checking-installer: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - name: Setup .NET Core + uses: actions/setup-dotnet@v3 + with: + dotnet-version: 7.0.x + - name: Check Installer run: | dotnet restore "./installer/installer.sln" dotnet format "./installer/installer.sln" --severity error --no-restore --verify-no-changes - + + dotnet-format-checking-launcher: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - name: Setup .NET Core + uses: actions/setup-dotnet@v3 + with: + dotnet-version: 6.0.x + - name: Check Launcher run: | dotnet restore "./launcher/launcher.sln" dotnet format "./launcher/launcher.sln" --severity error --no-restore --verify-no-changes + dotnet-format-checking-playback: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - name: Setup .NET Core + uses: actions/setup-dotnet@v3 + with: + dotnet-version: 6.0.x + - name: Check Playback run: | dotnet restore "./playback/playback.sln" diff --git a/installer/.gitignore b/installer/.gitignore index ec116cbf..154e1272 100644 --- a/installer/.gitignore +++ b/installer/.gitignore @@ -57,11 +57,14 @@ dlldata.c # Benchmark Results BenchmarkDotNet.Artifacts/ -# .NET Core +# .NET project.lock.json project.fragment.lock.json artifacts/ +# Tye +.tye/ + # ASP.NET Scaffolding ScaffoldingReadMe.txt @@ -397,5 +400,78 @@ FodyWeavers.xsd # JetBrains Rider *.sln.iml -#THUAI playback file -*.thuaipb +## +## Visual studio for Mac +## + + +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# Mac bundle stuff +*.dmg +*.app + +# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk diff --git a/installer/App.xaml b/installer/App.xaml new file mode 100644 index 00000000..1589d582 --- /dev/null +++ b/installer/App.xaml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/installer/App.xaml.cs b/installer/App.xaml.cs new file mode 100644 index 00000000..88ea998b --- /dev/null +++ b/installer/App.xaml.cs @@ -0,0 +1,12 @@ +namespace installer +{ + public partial class App : Application + { + public App() + { + InitializeComponent(); + + MainPage = new AppShell(); + } + } +} \ No newline at end of file diff --git a/installer/AppShell.xaml b/installer/AppShell.xaml new file mode 100644 index 00000000..7bd085d9 --- /dev/null +++ b/installer/AppShell.xaml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + diff --git a/installer/AppShell.xaml.cs b/installer/AppShell.xaml.cs new file mode 100644 index 00000000..1ad5cc6f --- /dev/null +++ b/installer/AppShell.xaml.cs @@ -0,0 +1,10 @@ +namespace installer +{ + public partial class AppShell : Shell + { + public AppShell() + { + InitializeComponent(); + } + } +} \ No newline at end of file diff --git a/installer/MauiProgram.cs b/installer/MauiProgram.cs new file mode 100644 index 00000000..fd4751b6 --- /dev/null +++ b/installer/MauiProgram.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.Logging; + +namespace installer +{ + public static class MauiProgram + { + public static MauiApp CreateMauiApp() + { + var builder = MauiApp.CreateBuilder(); + builder + .UseMauiApp() + .ConfigureFonts(fonts => + { + fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); + fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold"); + }); + +#if DEBUG + builder.Logging.AddDebug(); +#endif + + return builder.Build(); + } + } +} \ No newline at end of file diff --git a/installer/Model/Downloader.cs b/installer/Model/Downloader.cs new file mode 100644 index 00000000..b4e34d24 --- /dev/null +++ b/installer/Model/Downloader.cs @@ -0,0 +1,246 @@ +using COSXML.CosException; +using Newtonsoft.Json; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace installer.Model +{ + public struct UpdateInfo // 更新信息,包括新版本版本号、更改文件数和新文件数 + { + public string status; + public int changedFileCount; + public int newFileCount; + } + + public class Downloader + { + #region 属性区 + public class UserInfo + { + public string _id = ""; + public string email = ""; + } + public string ProgramName = "THUAI6"; // 要运行或下载的程序名称 + public string StartName = "maintest.exe"; // 启动的程序名 + private Local_Data Data; + private Tencent_Cos Cloud; + + private HttpClient Client = new HttpClient(); + private EEsast Web = new EEsast(); + + public enum UpdateStatus + { + success, unarchieving, downloading, hash_computing, error + } //{ newUser, menu, move, working, initializing, disconnected, error, successful, login, web, launch }; + public UpdateStatus Status; + + ConcurrentQueue downloadFile = new ConcurrentQueue(); // 需要下载的文件名 + ConcurrentQueue downloadFailed = new ConcurrentQueue(); //更新失败的文件名 + public List UpdateFailed + { + get { return downloadFailed.ToList(); } + } + public bool UpdatePlanned + { + get; set; + } + + public void ResetDownloadFailedInfo() + { + downloadFailed.Clear(); + } + + private int filenum = 0; // 总文件个数 + + public string Route { get; set; } + public string Username { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; + public string UserId { get => Web.ID; } + public string UserEmail { get => Web.Email; } + public string CodeRoute { get; set; } = string.Empty; + public string? Language { get; set; } = null; + public string PlayerNum { get; set; } = "nSelect"; + public enum LaunchLanguage { cpp, python }; + public LaunchLanguage launchLanguage { get; set; } = LaunchLanguage.cpp; + public enum UsingOS { Win, Linux, OSX }; + public UsingOS usingOS { get; set; } + public class Updater + { + public string Message; + public bool Working { get; set; } + public bool CombatCompleted { get => false; } + public bool UploadReady { get; set; } = false; + public bool ProfileAvailable { get; set; } + } + public bool LoginFailed { get; set; } = false; + public bool RememberMe { get; set; } + + #endregion + + #region 方法区 + public Downloader() + { + Data = new Local_Data(); + Route = Data.InstallPath; + Cloud = new Tencent_Cos("1314234950", "ap-beijing", "thuai6"); + Web.Token_Changed += SaveToken; + string temp; + if (Data.Config.TryGetValue("Remembered", out temp)) + { + if (Convert.ToBoolean(temp)) + { + if (Data.Config.TryGetValue("Username", out temp)) + Username = temp; + if (Data.Config.TryGetValue("Password", out temp)) + Password = temp; + } + } + } + + public void UpdateMD5() + { + if (File.Exists(Data.MD5DataPath)) + File.Delete(Data.MD5DataPath); + Status = UpdateStatus.downloading; + Cloud.DownloadFileAsync(Data.MD5DataPath, "hash.json").Wait(); + if (Cloud.Exceptions.Count > 0) + { + Status = UpdateStatus.error; + return; + } + Data.ReadMD5Data(); + } + + /// + /// 全新安装 + /// + public void Install() + { + UpdateMD5(); + if (Status == UpdateStatus.error) return; + + if (Directory.Exists(Data.InstallPath)) + Directory.Delete(Data.InstallPath, true); + + Data.Installed = false; + string zp = Path.Combine(Data.InstallPath, "THUAI7.tar.gz"); + Status = UpdateStatus.downloading; + Cloud.DownloadFileAsync(zp, "THUAI7.tar.gz").Wait(); + Status = UpdateStatus.unarchieving; + Cloud.ArchieveUnzip(zp, Data.InstallPath); + File.Delete(zp); + + Data.ResetInstallPath(Data.InstallPath); + Status = UpdateStatus.hash_computing; + Data.ScanDir(); + if (Data.MD5Update.Count != 0) + { + // TO DO: 下载文件与hash校验值不匹配修复 + Status = UpdateStatus.error; + Update(); + } + else + { + Status = UpdateStatus.success; + } + } + + /// + /// 检测是否需要进行更新 + /// 返回真时则表明需要更新 + /// + /// + public bool CheckUpdate() + { + UpdateMD5(); + Data.MD5Update.Clear(); + Status = UpdateStatus.hash_computing; + Data.ScanDir(); + return Data.MD5Update.Count != 0; + } + + /// + /// 更新文件 + /// + public void Update() + { + if (CheckUpdate()) + { + Status = UpdateStatus.downloading; + Cloud.DownloadQueueAsync(new ConcurrentQueue(Data.MD5Update), downloadFailed).Wait(); + if (downloadFailed.Count == 0) + { + Data.MD5Update.Clear(); + Status = UpdateStatus.hash_computing; + Data.ScanDir(); + if (Data.MD5Update.Count == 0) + { + Status = UpdateStatus.success; + return; + } + } + } + else + { + Status = UpdateStatus.success; + return; + } + Status = UpdateStatus.error; + } + + public async Task Login() + { + await Web.LoginToEEsast(Client, Username, Password); + } + + public void SaveToken(object? sender, EventArgs args) // 保存token + { + if (Data.Config.ContainsKey("Token")) + Data.Config["Token"] = Web.Token; + else + Data.Config.Add("Token", Web.Token); + Data.SaveConfig(); + } + + + public void RememberUser() + { + if (Data.Config.ContainsKey("Username")) + Data.Config["Username"] = Username; + else + Data.Config.Add("Username", Username); + + if (Data.Config.ContainsKey("Password")) + Data.Config["Password"] = Password; + else + Data.Config.Add("Password", Password); + + if (Data.Config.ContainsKey("Remembered")) + Data.Config["Remembered"] = "true"; + else + Data.Config.Add("Remembered", "true"); + + Data.SaveConfig(); + } + public void ForgetUser() + { + if (Data.Config.ContainsKey("Remembered")) + Data.Config["Remembered"] = "false"; + + if (Data.Config.ContainsKey("Username")) + Data.Config["Username"] = string.Empty; + + if (Data.Config.ContainsKey("Password")) + Data.Config["Password"] = string.Empty; + + Data.SaveConfig(); + } + #endregion + } + + +} diff --git a/installer/Model/EEsast.cs b/installer/Model/EEsast.cs new file mode 100644 index 00000000..03cc0782 --- /dev/null +++ b/installer/Model/EEsast.cs @@ -0,0 +1,226 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http.Json; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using COSXML.Auth; + +namespace installer.Model +{ + [Serializable] + record LoginResponse + { + // Map `Token` to `token` when serializing + + public string Token { get; set; } = ""; + } + + class EEsast + { + public enum language { cpp, py }; + private string token = string.Empty; + public string Token + { + get => token; protected set + { + if (token != value) + Token_Changed.Invoke(this, new EventArgs()); + token = value; + } + } + public event EventHandler Token_Changed; + public string ID { get; protected set; } + public string Email { get; protected set; } + + public ConcurrentQueue Exceptions = new ConcurrentQueue(); + public enum WebStatus + { + disconnected, offline, logined + } + public WebStatus Status = WebStatus.disconnected; + public Tencent_Cos EEsast_Cos { get; protected set; } + public async Task LoginToEEsast(HttpClient client, string useremail, string userpassword) + { + EEsast_Cos = new Tencent_Cos("1255334966", "ap-beijing", "eesast"); + try + { + using (var response = await client.PostAsync("https://api.eesast.com/users/login", JsonContent.Create(new + { + email = useremail, + password = userpassword, + }))) + { + switch (response.StatusCode) + { + case System.Net.HttpStatusCode.OK: + var info = Helper.DeserializeJson1>(await response.Content.ReadAsStringAsync()); + ID = info.Keys.Contains("_id") ? info["_id"] : string.Empty; + Email = info.Keys.Contains("email") ? info["email"] : string.Empty; + Token = info.Keys.Contains("token") ? info["token"] : string.Empty; + Status = WebStatus.logined; + break; + default: + int code = ((int)response.StatusCode); + if (code == 401) + { + Exceptions.Enqueue(new Exception("邮箱或密码错误!")); + } + else + { + Exceptions.Enqueue(new Exception($"HTTP错误,错误码:{code}")); + } + break; + } + } + } + catch (Exception ex) + { + Exceptions.Enqueue(ex); + } + } + + /// + /// + /// + /// http client + /// 代码源位置 + /// 编程语言,格式为"cpp"或"python" + /// 第x位玩家,格式为"player_x" + /// -1:tokenFail;-2:FileNotExist;-3:CosFail;-4:loginTimeout;-5:Fail;-6:ReadFileFail;-7:networkError + async public Task UploadFiles(HttpClient client, string tarfile, string type, string plr) //用来上传文件 + { + if (Status != WebStatus.logined) // + { + Exceptions.Append(new UnauthorizedAccessException("用户未登录")); + return -1; + } + try + { + string content; + client.DefaultRequestHeaders.Authorization = new("Bearer", Token); + if (!File.Exists(tarfile)) + { + Exceptions.Append(new IOException("用户不存在")); + return -2; + } + using FileStream fs = new FileStream(tarfile, FileMode.Open, FileAccess.Read); + using StreamReader sr = new StreamReader(fs); + content = sr.ReadToEnd(); + string targetUrl = $"https://api.eesast.com/static/player?team_id={await GetTeamId()}"; + using (var response = await client.GetAsync(targetUrl)) + { + switch (response.StatusCode) + { + case System.Net.HttpStatusCode.OK: + var res = Helper.DeserializeJson1>(await response.Content.ReadAsStringAsync()); + string tmpSecretId = res["TmpSecretId"]; //"临时密钥 SecretId"; + string tmpSecretKey = res["TmpSecretKey"]; //"临时密钥 SecretKey"; + string tmpToken = res["SecurityToken"]; //"临时密钥 token"; + long tmpExpiredTime = Convert.ToInt64(res["ExpiredTime"]); //临时密钥有效截止时间,精确到秒 + QCloudCredentialProvider cosCredentialProvider = new DefaultSessionQCloudCredentialProvider( + tmpSecretId, tmpSecretKey, tmpExpiredTime, tmpToken + ); + EEsast_Cos.UpdateSecret(cosCredentialProvider); + + string cosPath = $"/THUAI7/{GetTeamId()}/{type}/{plr}"; //对象在存储桶中的位置标识符,即称对象键 + string srcPath = tarfile;//本地文件绝对路径 + EEsast_Cos.UploadFileAsync(srcPath, cosPath).Wait(); + + break; + case System.Net.HttpStatusCode.Unauthorized: + //Console.WriteLine("您未登录或登录过期,请先登录"); + return -4; + default: + //Console.WriteLine("上传失败!"); + return -5; + } + } + } + catch (IOException) + { + //Console.WriteLine("文件读取错误!请检查文件是否被其它应用占用!"); + return -6; + } + catch + { + //Console.WriteLine("请求错误!请检查网络连接!"); + return -7; + } + return 0; + } + + async public Task UserDetails(HttpClient client) // 用来测试访问网站 + { + if (Status != WebStatus.logined) // 读取token失败 + { + Exceptions.Append(new UnauthorizedAccessException("用户未登录")); + return; + } + try + { + client.DefaultRequestHeaders.Authorization = new("Bearer", Token); + Console.WriteLine(Token); + using (var response = await client.GetAsync("https://api.eesast.com/application/info")) + { + switch (response.StatusCode) + { + case System.Net.HttpStatusCode.OK: + Console.WriteLine("Require OK"); + Console.WriteLine(await response.Content.ReadAsStringAsync()); + break; + default: + int code = ((int)response.StatusCode); + if (code == 401) + { + Console.WriteLine("您未登录或登录过期,请先登录"); + } + return; + } + } + } + catch + { + Console.WriteLine("请求错误!请检查网络连接!"); + } + } + + async public Task GetTeamId() + { + var client = new HttpClient(); + var request = new HttpRequestMessage(HttpMethod.Post, "https://api.eesast.com/dev/v1/graphql"); + request.Headers.Add("x-hasura-admin-secret", "hasuraDevAdminSecret"); + //var content = new StringContent($@" + // {{ + // ""query"": ""query MyQuery {{contest_team_member(where: {{user_id: {{_eq: \""{Downloader.UserInfo._id}\""}}}}) {{ team_id }}}}"", + // ""variables"": {{}}, + // }}", null, "application/json"); + var content = new StringContent("{\"query\":\"query MyQuery {\\r\\n contest_team_member(where: {user_id: {_eq: \\\"" + ID + "\\\"}}) {\\r\\n team_id\\r\\n }\\r\\n}\",\"variables\":{}}", null, "application/json"); + request.Content = content; + var response = await client.SendAsync(request); + response.EnsureSuccessStatusCode(); + var info = await response.Content.ReadAsStringAsync(); + var s1 = Helper.DeserializeJson1>(info)["data"]; + var s2 = Helper.DeserializeJson1>>(s1.ToString() ?? "")["contest_team_member"]; + var sres = Helper.DeserializeJson1>(s2[0].ToString() ?? "")["team_id"]; + return sres; + } + + public async Task GetUserId(string learnNumber) + { + var client = new HttpClient(); + var request = new HttpRequestMessage(HttpMethod.Post, "https://api.eesast.com/dev/v1/graphql"); + request.Headers.Add("x-hasura-admin-secret", "hasuraDevAdminSecret"); + var content = new StringContent("{\"query\":\"query MyQuery {\r\n user(where: {id: {_eq: \"" + + learnNumber + "\"}}) {\r\n _id\r\n }\r\n}\r\n\",\"variables\":{}}", null, "application/json"); + request.Content = content; + var response = await client.SendAsync(request); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsStringAsync(); + } + + + } +} diff --git a/installer/Model/Helper.cs b/installer/Model/Helper.cs new file mode 100644 index 00000000..b0e8137f --- /dev/null +++ b/installer/Model/Helper.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +namespace installer.Model +{ + internal static class Helper + { + public static T DeserializeJson1(string json) + where T : notnull + { + return JsonConvert.DeserializeObject(json) + ?? throw new Exception("Failed to deserialize json."); + } + + public static T? TryDeserializeJson(string json) + where T : notnull + { + return JsonConvert.DeserializeObject(json); + } + + public static string GetFileMd5Hash(string strFileFullPath) + { + FileStream? fst = null; + try + { + fst = new FileStream(strFileFullPath, FileMode.Open, FileAccess.Read); + byte[] data = MD5.Create().ComputeHash(fst); + + StringBuilder sBuilder = new StringBuilder(); + + for (int i = 0; i < data.Length; i++) + { + sBuilder.Append(data[i].ToString("x2")); + } + + fst.Close(); + return sBuilder.ToString().ToLower(); + } + catch (Exception) + { + if (fst != null) + fst.Close(); + if (File.Exists(strFileFullPath)) + return "conflict"; + return ""; + } + finally + { + } + } + + public static string ConvertAbsToRel(string basePath, string fullPath) + { + if (fullPath.StartsWith(basePath)) + { + fullPath.Replace(basePath, "."); + } + return fullPath; + } + } +} diff --git a/installer/Model/Local_Data.cs b/installer/Model/Local_Data.cs new file mode 100644 index 00000000..e987b841 --- /dev/null +++ b/installer/Model/Local_Data.cs @@ -0,0 +1,206 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Maui.Platform; +using Newtonsoft.Json; +using static System.Runtime.InteropServices.JavaScript.JSType; + +namespace installer.Model +{ + class Local_Data + { + public string ConfigPath; // 标记路径记录文件THUAI7.json的路径 + public string MD5DataPath; // 标记MD5本地文件缓存值 + public Dictionary Config + { + get; protected set; + } + public Dictionary MD5Data + { + get; protected set; + }// 路径为尽可能相对路径 + public ConcurrentBag MD5Update + { + get; set; + }// 路径为绝对路径 + public string InstallPath = ""; // 最后一级为THUAI7文件夹所在目录 + public bool Installed = false; // 项目是否安装 + public Local_Data() + { + MD5Update = new ConcurrentBag(); + ConfigPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), + "THUAI7.json"); + if (File.Exists(ConfigPath)) + { + ReadConfig(); + if (Config.ContainsKey("InstallPath") && Directory.Exists(Config["InstallPath"])) + { + InstallPath = Config["InstallPath"].Replace('\\', '/'); + if (Config.ContainsKey("MD5DataPath")) + { + MD5DataPath = Config["MD5DataPath"].StartsWith('.') ? + Path.Combine(InstallPath, Config["MD5DataPath"]) : + Config["MD5DataPath"]; + ReadMD5Data(); + } + else + { + MD5DataPath = Path.Combine(InstallPath, "./hash.json"); + Config["MD5DataPath"] = "./hash.json"; + SaveMD5Data(); + SaveConfig(); + } + Installed = true; + } + else + { + MD5DataPath = Path.Combine(InstallPath, "./hash.json"); + Config["MD5DataPath"] = "./hash.json"; + SaveMD5Data(); + SaveConfig(); + } + } + else + { + Config = new Dictionary + { + { "THUAI7", "2024" }, + { "MD5DataPath", "./hash.json" } + }; + MD5DataPath = Path.Combine(InstallPath, "./hash.json"); + SaveMD5Data(); + SaveConfig(); + } + } + + ~Local_Data() + { + SaveConfig(); + } + + public void ResetInstallPath(string newPath) + { + if (!Directory.Exists(Path.GetDirectoryName(newPath))) + { + Directory.CreateDirectory(Path.GetDirectoryName(newPath)); + } + if (Installed) + { + // 移动已有文件夹至新位置 + Directory.Move(newPath, InstallPath); + } + InstallPath = newPath.Replace('\\', '/'); + if (Config.ContainsKey("InstallPath")) + Config["InstallPath"] = InstallPath; + else + Config.Add("InstallPath", InstallPath); + SaveConfig(); + } + + public static bool IsUserFile(string filename) + { + if (filename.Substring(filename.Length - 3, 3).Equals(".sh") || filename.Substring(filename.Length - 4, 4).Equals(".cmd")) + return true; + if (filename.Equals("AI.cpp") || filename.Equals("AI.py")) + return true; + return false; + } + + public void ReadConfig() + { + using (StreamReader r = new StreamReader(ConfigPath)) + { + string json = r.ReadToEnd(); + if (json == null || json == "") + { + json += @"{""THUAI6""" + ":" + @"""2023""}"; + } + Config = Helper.TryDeserializeJson>(json) ?? new Dictionary(); + } + } + + public void SaveConfig() + { + using FileStream fs = new FileStream(ConfigPath, FileMode.OpenOrCreate, FileAccess.ReadWrite); + using StreamWriter sw = new StreamWriter(fs); + fs.SetLength(0); + sw.Write(JsonConvert.SerializeObject(Config)); + sw.Flush(); + } + + public void ReadMD5Data() + { + var newMD5Data = new Dictionary(); + using (StreamReader r = new StreamReader(MD5DataPath)) + { + string json = r.ReadToEnd(); + if (json == null || json == "") + { + newMD5Data = new Dictionary(); + } + else + { + newMD5Data = Helper.TryDeserializeJson>(json) ?? new Dictionary(); + } + } + foreach (var item in newMD5Data) + { + if (MD5Data.ContainsKey(item.Key)) + { + if (MD5Data[item.Key] != newMD5Data[item.Value]) + { + MD5Data[item.Key] = newMD5Data[item.Value]; + MD5Update.Add(Path.Combine(InstallPath, item.Key)); + } + } + else + { + MD5Data.Add(item.Key, item.Value); + MD5Update.Add(Path.Combine(InstallPath, item.Key)); + } + } + } + + public void SaveMD5Data() + { + using FileStream fs = new FileStream(MD5DataPath, FileMode.OpenOrCreate, FileAccess.ReadWrite); + using StreamWriter sw = new StreamWriter(fs); + fs.SetLength(0); + sw.Write(JsonConvert.SerializeObject(MD5Data)); + sw.Flush(); + } + + public void ScanDir() => ScanDir(InstallPath); + + public void ScanDir(string dir) + { + var d = new DirectoryInfo(dir); + foreach (var file in d.GetFiles()) + { + var relFile = Helper.ConvertAbsToRel(InstallPath, file.FullName); + // 用户自己的文件不会被计入更新hash数据中 + if (IsUserFile(file.Name)) + continue; + var hash = Helper.GetFileMd5Hash(file.FullName); + if (MD5Data.Keys.Contains(relFile)) + { + if (MD5Data[relFile] != hash) + { + MD5Data[relFile] = hash; + MD5Update.Add(file.FullName); + } + } + else + { + MD5Data.Add(relFile, hash); + MD5Update.Add(file.FullName); + } + } + foreach (var d1 in d.GetDirectories()) { ScanDir(d1.FullName); } + } + } +} diff --git a/installer/Model/Run_Program.cs b/installer/Model/Run_Program.cs new file mode 100644 index 00000000..f63a30c9 --- /dev/null +++ b/installer/Model/Run_Program.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace installer.Model +{ + class Run_Program + { + } +} diff --git a/installer/Model/Tencent_Cos.cs b/installer/Model/Tencent_Cos.cs new file mode 100644 index 00000000..359d5784 --- /dev/null +++ b/installer/Model/Tencent_Cos.cs @@ -0,0 +1,172 @@ +using COSXML; +using COSXML.Auth; +using COSXML.CosException; +using COSXML.Model.Object; +using ICSharpCode.SharpZipLib; +using ICSharpCode.SharpZipLib.Tar; +using ICSharpCode.SharpZipLib.GZip; +using Newtonsoft.Json; +using System.Collections.Concurrent; +using COSXML.Common; +using COSXML.Transfer; + +namespace installer.Model +{ + public class Tencent_Cos + { + public string Appid { get; init; } // 设置腾讯云账户的账户标识(APPID) + public string Region { get; init; } // 设置一个默认的存储桶地域 + public string BucketName { get; set; } + public ConcurrentStack Exceptions { get; set; } + + private string secretId = "***"; //"云 API 密钥 SecretId"; + private string secretKey = "***"; //"云 API 密钥 SecretKey"; + protected CosXmlServer cosXml; + + public Tencent_Cos(string appid, string region, string bucketName) + { + Appid = appid; Region = region; BucketName = bucketName; + Exceptions = new ConcurrentStack(); + // 初始化CosXmlConfig(提供配置SDK接口) + var config = new CosXmlConfig.Builder() + .IsHttps(true) // 设置默认 HTTPS 请求 + .SetAppid(Appid) // 设置腾讯云账户的账户标识 APPID + .SetRegion(Region) // 设置一个默认的存储桶地域 + .SetDebugLog(true) // 显示日志 + .Build(); // 创建 CosXmlConfig 对象 + long durationSecond = 1000; // 每次请求签名有效时长,单位为秒 + QCloudCredentialProvider cosCredentialProvider = new DefaultQCloudCredentialProvider(secretId, secretKey, durationSecond); + // 初始化 CosXmlServer + cosXml = new CosXmlServer(config, cosCredentialProvider); + } + + public void UpdateSecret(QCloudCredentialProvider credential) + { + var config = new CosXmlConfig.Builder() + .IsHttps(true) // 设置默认 HTTPS 请求 + .SetAppid(Appid) // 设置腾讯云账户的账户标识 APPID + .SetRegion(Region) // 设置一个默认的存储桶地域 + .SetDebugLog(true) // 显示日志 + .Build(); // 创建 CosXmlConfig 对象 + cosXml = new CosXmlServer(config, credential); + } + + public async Task DownloadFileAsync(string savePath, string remotePath = null) + { + // download_dir标记根文件夹路径,key为相对根文件夹的路径(不带./) + // 创建存储桶 + try + { + // 覆盖对应文件,如果无法覆盖则报错 + if (File.Exists(savePath)) + File.Delete(savePath); + string bucket = $"{BucketName}-{Appid}"; // 格式:BucketName-APPID + string localDir = Path.GetDirectoryName(savePath) // 本地文件夹 + ?? throw new Exception("本地文件夹路径获取失败"); + string localFileName = Path.GetFileName(savePath); // 指定本地保存的文件名 + GetObjectRequest request = new GetObjectRequest(bucket, remotePath ?? localFileName, localDir, localFileName); + + Dictionary test = request.GetRequestHeaders(); + request.SetCosProgressCallback(delegate (long completed, long total) + { + //Console.WriteLine(String.Format("progress = {0:##.##}%", completed * 100.0 / total)); + }); + // 执行请求 + GetObjectResult result = cosXml.GetObject(request); + // 请求成功 + } + catch (Exception ex) + { + Exceptions.Push(ex); + throw; + //MessageBox.Show($"下载{download_dir}时出现未知问题,请反馈"); + } + } + + public async Task DownloadQueueAsync(ConcurrentQueue queue, ConcurrentQueue downloadFailed) + { + ThreadPool.SetMaxThreads(20, 20); + for (int i = 0; i < queue.Count; i++) + { + string item; + queue.TryDequeue(out item); + ThreadPool.QueueUserWorkItem(async _ => + { + try + { + await DownloadFileAsync(item); + } + catch (Exception ex) + { + downloadFailed.Enqueue(item); + } + }); + } + } + + public void ArchieveUnzip(string zipPath, string targetDir) + { + Stream? inStream = null; + Stream? gzipStream = null; + TarArchive? tarArchive = null; + try + { + using (inStream = File.OpenRead(zipPath)) + { + using (gzipStream = new GZipInputStream(inStream)) + { + tarArchive = TarArchive.CreateInputTarArchive(gzipStream); + tarArchive.ExtractContents(targetDir); + tarArchive.Close(); + } + } + } + catch + { + //出错 + } + finally + { + if (tarArchive != null) tarArchive.Close(); + if (gzipStream != null) gzipStream.Close(); + if (inStream != null) inStream.Close(); + } + } + + public async Task UploadFileAsync(string localPath, string targetPath) + { + // 初始化 TransferConfig + TransferConfig transferConfig = new TransferConfig(); + + // 初始化 TransferManager + TransferManager transferManager = new TransferManager(cosXml, transferConfig); + + string bucket = $"{BucketName}-{Appid}"; + + COSXMLUploadTask uploadTask = new COSXMLUploadTask(bucket, targetPath); + + uploadTask.SetSrcPath(localPath); + + uploadTask.progressCallback = delegate (long completed, long total) + { + //Console.WriteLine(string.Format("progress = {0:##.##}%", completed * 100.0 / total)); + }; + + COSXMLUploadTask.UploadTaskResult result = await transferManager.UploadAsync(uploadTask); + + try + { + COSXMLUploadTask.UploadTaskResult r = await transferManager.UploadAsync(uploadTask); + //Console.WriteLine(result.GetResultInfo()); + string eTag = r.eTag; + //到这里应该是成功了,但是因为我没有试过,也不知道具体情况,可能还要根据result的内容判断 + } + catch (Exception ex) + { + Exceptions.Push(ex); + throw; + } + + } + } +} diff --git a/installer/Page/CompetitionPage.xaml b/installer/Page/CompetitionPage.xaml new file mode 100644 index 00000000..51d1ffde --- /dev/null +++ b/installer/Page/CompetitionPage.xaml @@ -0,0 +1,24 @@ + + + +