From 597d6731e4e3150883f3105749782fa25b77b6f6 Mon Sep 17 00:00:00 2001 From: Kalle Date: Tue, 19 Nov 2024 13:06:55 +0100 Subject: [PATCH] improvement: optimize plugin updates to allow multiple sources. Currently implemented: GitHub --- src/UpdateManager+Commands.cs | 2 +- src/UpdateManager.cs | 262 +++++++++++++++++++--------------- src/lang/en.json | 1 + 3 files changed, 150 insertions(+), 115 deletions(-) diff --git a/src/UpdateManager+Commands.cs b/src/UpdateManager+Commands.cs index 724c933..f7578a3 100644 --- a/src/UpdateManager+Commands.cs +++ b/src/UpdateManager+Commands.cs @@ -13,7 +13,7 @@ public void CommandUpdatePlugins(CCSPlayerController player, CommandInfo command // update plugin list getPluginList(); // check for updates - checkForUpdates(); + UpdateAllPlugins(true).GetAwaiter().GetResult(); } } } diff --git a/src/UpdateManager.cs b/src/UpdateManager.cs index 9edd812..aee322e 100644 --- a/src/UpdateManager.cs +++ b/src/UpdateManager.cs @@ -31,7 +31,7 @@ public override void Load(bool hotReload) // register listeners RegisterListeners(); // check on startup if enabled - if (Config.CheckOnStartup) checkForUpdates(); + if (Config.CheckOnStartup) UpdateAllPlugins(true).GetAwaiter().GetResult(); } public override void Unload(bool hotReload) @@ -63,7 +63,7 @@ private void OnMapStart(string mapName) UpdateConfig(); SaveConfig(); // check for updates - checkForUpdates(); + UpdateAllPlugins(true).GetAwaiter().GetResult(); } private void OnMapEnd() @@ -75,7 +75,7 @@ private void OnMapEnd() UpdateConfig(); SaveConfig(); // check for updates - checkForUpdates(); + UpdateAllPlugins(true).GetAwaiter().GetResult(); } private void OnServerHibernationUpdate(bool isHibernating) @@ -88,7 +88,7 @@ private void OnServerHibernationUpdate(bool isHibernating) UpdateConfig(); SaveConfig(); // check for updates - checkForUpdates(); + UpdateAllPlugins(true).GetAwaiter().GetResult(); } private void getPluginList() @@ -107,8 +107,20 @@ private void getPluginList() .Build(); using var reader = new StreamReader(pluginFilePath); var yamlObject = deserializer.Deserialize>(reader); - var pluginVersion = yamlObject["version"]; - var pluginRepoURL = yamlObject["repository"]; + if (!yamlObject.TryGetValue("version", out var pluginVersion)) + { + Console.WriteLine(Localizer["update.error"].Value + .Replace("{pluginName}", pluginName) + .Replace("{error}", "Version not found in .info file.")); + continue; + } + if (!yamlObject.TryGetValue("repository", out var pluginRepoURL)) + { + Console.WriteLine(Localizer["update.error"].Value + .Replace("{pluginName}", pluginName) + .Replace("{error}", "Repository-URL not found in .info file.")); + continue; + } // add to plugin list _plugins.Add(new Tuple(pluginName, pluginVersion, pluginRepoURL)); Console.WriteLine(Localizer["plugin.found"].Value @@ -118,117 +130,139 @@ private void getPluginList() } } - private void checkForUpdates() + private async Task UpdatePluginOnGithub(string pluginName, bool applyUpdate) { - Task.Run(async () => + // find plugin in list + var plugin = _plugins.FirstOrDefault(p => p.Item1 == pluginName); + if (plugin == null) return false; + // get plugin details + var (name, version, repoURL) = plugin; + // check for plugin configuration + var pluginConfig = Config.Plugins[name]; + if (pluginConfig == null || !pluginConfig.Enabled) return false; + // check for updates + try { - foreach (var (pluginName, pluginVersion, pluginRepoURL) in _plugins) + var client = new HttpClient(); + client.DefaultRequestHeaders.Add("User-Agent", "CounterStrikeSharp"); + if (!string.IsNullOrEmpty(pluginConfig.GithubToken)) + client.DefaultRequestHeaders.Add("Authorization", $"token {pluginConfig.GithubToken}"); + else if (!string.IsNullOrEmpty(Config.GithubToken)) + client.DefaultRequestHeaders.Add("Authorization", $"token {Config.GithubToken}"); + + var repoPath = new Uri(repoURL).AbsolutePath.Trim('/'); + var response = await client.GetAsync($"https://api.github.com/repos/{repoPath}/releases/latest"); + + // error due to http error + if (!response.IsSuccessStatusCode) { - // get plugin configuration - var pluginConfig = Config.Plugins[pluginName]; - if (pluginConfig == null || !pluginConfig.Enabled) continue; - // check github api /repos/{owner}/{repo}/releases/latest - try - { - var client = new HttpClient(); - client.DefaultRequestHeaders.Add("User-Agent", "CounterStrikeSharp"); - // check for plugin github token - if (!string.IsNullOrEmpty(pluginConfig.GithubToken)) - client.DefaultRequestHeaders.Add("Authorization", $"token {pluginConfig.GithubToken}"); - // check for global github token - else if (!string.IsNullOrEmpty(Config.GithubToken)) - client.DefaultRequestHeaders.Add("Authorization", $"token {Config.GithubToken}"); - // get latest release - var repoPath = new Uri(pluginRepoURL).AbsolutePath.Trim('/'); - var response = await client.GetAsync($"https://api.github.com/repos/{repoPath}/releases/latest"); - // check if response is successful - if (!response.IsSuccessStatusCode) - { - Console.WriteLine(Localizer["update.error"].Value - .Replace("{pluginName}", pluginName) - .Replace("{error}", response.ReasonPhrase)); - continue; - } - // parse response - var responseString = await response.Content.ReadAsStringAsync(); - // get download url for latest .zip - var release = JsonSerializer.Deserialize>(responseString); - if (release == null) - { - Console.WriteLine(Localizer["update.error"].Value - .Replace("{pluginName}", pluginName) - .Replace("{error}", "Release data not found.")); - continue; - } - if (!release.TryGetValue("tag_name", out var tagName)) - { - Console.WriteLine(Localizer["update.error"].Value - .Replace("{pluginName}", pluginName) - .Replace("{error}", "Tag name not found in release data.")); - continue; - } - var latestVersion = tagName.ToString(); - if (latestVersion == pluginVersion) - { - Console.WriteLine(Localizer["update.notfound"].Value - .Replace("{pluginName}", pluginName) - .Replace("{pluginVersion}", pluginVersion)); - continue; - } - Console.WriteLine(Localizer["update.available"].Value - .Replace("{pluginName}", pluginName) - .Replace("{pluginVersion}", pluginVersion) - .Replace("{latestVersion}", latestVersion)); - // download and update plugin - if (!release.TryGetValue("assets", out var assets) || assets == null) - { - Console.WriteLine(Localizer["update.error"].Value - .Replace("{pluginName}", pluginName) - .Replace("{error}", "Assets not found in release data.")); - continue; - } - // get asset list - var assetList = JsonSerializer.Deserialize>>(assets?.ToString() ?? string.Empty); - // look for zip asset - var zipAsset = assetList?.FirstOrDefault(a => - { - if (a == null) return false; - return a.TryGetValue("name", out var name) && name != null && name.ToString()!.EndsWith(".zip"); - }); - // check if zip asset was found - if (zipAsset == null || !zipAsset.TryGetValue("browser_download_url", out var browserDownloadUrl)) - { - Console.WriteLine(Localizer["update.error"].Value - .Replace("{pluginName}", pluginName) - .Replace("{error}", "Download URL for .zip file not found in assets.")); - continue; - } - // download zip - var downloadURL = browserDownloadUrl.ToString(); - var downloadPath = Path.Combine(_pluginPath, $"{pluginName}.zip"); - var downloadStream = await client.GetStreamAsync(downloadURL); - // save zip - using (var fileStream = File.Create(downloadPath)) - { - await downloadStream.CopyToAsync(fileStream); - } - // extract zip - ZipFile.ExtractToDirectory(downloadPath, _pluginPath, true); - // remove zip - File.Delete(downloadPath); - Console.WriteLine(Localizer["update.success"].Value - .Replace("{pluginName}", pluginName) - .Replace("{pluginVersion}", pluginVersion) - .Replace("{latestVersion}", latestVersion)); - } - catch (Exception e) - { - Console.WriteLine(Localizer["update.error"].Value - .Replace("{pluginName}", pluginName) - .Replace("{error}", e.Message)); - } + Console.WriteLine(Localizer["update.error"].Value + .Replace("{pluginName}", name) + .Replace("{error}", response.ReasonPhrase)); + return false; + } + // read response + var responseString = await response.Content.ReadAsStringAsync(); + var release = JsonSerializer.Deserialize>(responseString); + // error due to missing data + if (release == null || !release.TryGetValue("tag_name", out var tagName)) + { + Console.WriteLine(Localizer["update.error"].Value + .Replace("{pluginName}", name) + .Replace("{error}", "Release data not found.")); + return false; } - }); + // check for version differences + var latestVersion = tagName.ToString(); + if (latestVersion == version) + { + Console.WriteLine(Localizer["update.notfound"].Value + .Replace("{pluginName}", name) + .Replace("{pluginVersion}", version)); + return false; + } + // update available + Console.WriteLine(Localizer["update.available"].Value + .Replace("{pluginName}", name) + .Replace("{pluginVersion}", version) + .Replace("{latestVersion}", latestVersion)); + // stop if no update should be applied + if (!applyUpdate) return true; + // check for assets + if (!release.TryGetValue("assets", out var assets) || assets == null) + { + Console.WriteLine(Localizer["update.error"].Value + .Replace("{pluginName}", name) + .Replace("{error}", "Assets not found in release data.")); + return false; + } + // check for zip assets + var assetList = JsonSerializer.Deserialize>>(assets?.ToString() ?? string.Empty); + var zipAsset = assetList?.FirstOrDefault(a => + { + if (a == null) return false; + return a.TryGetValue("name", out var assetName) && assetName != null && assetName.ToString()!.EndsWith(".zip"); + }); + // check if download url exists + if (zipAsset == null || !zipAsset.TryGetValue("browser_download_url", out var browserDownloadUrl)) + { + Console.WriteLine(Localizer["update.error"].Value + .Replace("{pluginName}", name) + .Replace("{error}", "Download URL for .zip file not found in assets.")); + return false; + } + // download zip + var downloadURL = browserDownloadUrl.ToString(); + var downloadPath = Path.Combine(_pluginPath, $"{name}.zip"); + var downloadStream = await client.GetStreamAsync(downloadURL); + // save zip + using (var fileStream = File.Create(downloadPath)) + { + await downloadStream.CopyToAsync(fileStream); + } + // extract zip + ZipFile.ExtractToDirectory(downloadPath, _pluginPath, true); + File.Delete(downloadPath); + // indicate success + Console.WriteLine(Localizer["update.success"].Value + .Replace("{pluginName}", name) + .Replace("{pluginVersion}", version) + .Replace("{latestVersion}", latestVersion)); + + return true; + } + catch (Exception e) + { + // error during update + Console.WriteLine(Localizer["update.error"].Value + .Replace("{pluginName}", name) + .Replace("{error}", e.Message)); + return false; + } + } + + private async Task UpdatePlugin(string pluginName, bool applyUpdate) + { + // find plugin in list + var plugin = _plugins.FirstOrDefault(p => p.Item1 == pluginName); + if (plugin == null) return; + // get plugin details + var (name, version, repoURL) = plugin; + // check if repoURL is github + if (repoURL.Contains("github.com")) + await UpdatePluginOnGithub(pluginName, applyUpdate); + else + Console.WriteLine(Localizer["update.error"].Value + .Replace("{pluginName}", name) + .Replace("{error}", "Only Github repositories are supported.")); + } + + private async Task UpdateAllPlugins(bool applyUpdate) + { + foreach (var plugin in _plugins) + { + await UpdatePlugin(plugin.Item1, applyUpdate); + } } } -} +} \ No newline at end of file diff --git a/src/lang/en.json b/src/lang/en.json index d996e43..1219d89 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -3,6 +3,7 @@ "core.unload": "[UpdateManager] Unloaded Plugin!", "config.loaded": "[UpdateManager] Loaded configuration!", "plugin.found": "[UpdateManager] Found plugin: {pluginName} v{pluginVersion}", + "plugin.error": "[UpdateManager] Error while adding plugin {pluginName}: {error}", "update.error": "[UpdateManager] Error while updating plugin {pluginName}: {error}", "update.available": "[UpdateManager] New version available: {pluginName} v{latestVersion} (current: v{pluginVersion})", "update.notfound": "[UpdateManager] {pluginName} v{pluginVersion} is already the newest version",