-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement plugin updates with
IPluginUpdates
interface (#3151)
* Initial implementation of plugin updates * Update PluginsCore.cs * Update IPluginUpdates.cs * Update PluginsCore.cs * Make it work * Misc * Revert "Misc" This reverts commit bccd1bb. * Proper fix * Make plugin updates independent of GitHub * Final touches * Misc * Allow plugin creators for more flexibility in picking from GitHub releases * Misc rename * Make changelog internal again This is ASF implementation detail, make body available instead and let people implement changelogs themselves * Misc * Add missing localization * Add a way to disable plugin updates * Update PluginsCore.cs * Update PluginsCore.cs * Misc * Update IGitHubPluginUpdates.cs * Update IGitHubPluginUpdates.cs * Update IGitHubPluginUpdates.cs * Update IGitHubPluginUpdates.cs * Make zip selection ignore case * Update ArchiSteamFarm/Core/Utilities.cs Co-authored-by: Vita Chumakova <[email protected]> * Misc error notify * Add commands and finally call it a day * Misc progress percentages text * Misc * Flip DefaultPluginsUpdateMode as per the voting * Misc --------- Co-authored-by: Vita Chumakova <[email protected]>
- Loading branch information
Showing
18 changed files
with
1,358 additions
and
556 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,18 +1,20 @@ | ||
// ---------------------------------------------------------------------------------------------- | ||
// _ _ _ ____ _ _____ | ||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ | ||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ | ||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | | ||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| | ||
// | | ||
// ---------------------------------------------------------------------------------------------- | ||
// | ||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki | ||
// Contact: [email protected] | ||
// | | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
|
@@ -26,6 +28,7 @@ | |
using System.Diagnostics.CodeAnalysis; | ||
using System.Globalization; | ||
using System.IO; | ||
using System.IO.Compression; | ||
using System.Linq; | ||
using System.Net; | ||
using System.Resources; | ||
|
@@ -35,6 +38,7 @@ | |
using AngleSharp.Dom; | ||
using AngleSharp.XPath; | ||
using ArchiSteamFarm.Localization; | ||
using ArchiSteamFarm.NLog; | ||
using ArchiSteamFarm.Storage; | ||
using Humanizer; | ||
using Humanizer.Localisation; | ||
|
@@ -263,26 +267,6 @@ public static bool TryReadJsonWebToken(string token, [NotNullWhen(true)] out Jso | |
return true; | ||
} | ||
|
||
internal static void DeleteEmptyDirectoriesRecursively(string directory) { | ||
ArgumentException.ThrowIfNullOrEmpty(directory); | ||
|
||
if (!Directory.Exists(directory)) { | ||
return; | ||
} | ||
|
||
try { | ||
foreach (string subDirectory in Directory.EnumerateDirectories(directory)) { | ||
DeleteEmptyDirectoriesRecursively(subDirectory); | ||
} | ||
|
||
if (!Directory.EnumerateFileSystemEntries(directory).Any()) { | ||
Directory.Delete(directory); | ||
} | ||
} catch (Exception e) { | ||
ASF.ArchiLogger.LogGenericException(e); | ||
} | ||
} | ||
|
||
internal static ulong MathAdd(ulong first, int second) { | ||
if (second >= 0) { | ||
return first + (uint) second; | ||
|
@@ -291,16 +275,17 @@ internal static ulong MathAdd(ulong first, int second) { | |
return first - (uint) -second; | ||
} | ||
|
||
internal static bool RelativeDirectoryStartsWith(string directory, params string[] prefixes) { | ||
ArgumentException.ThrowIfNullOrEmpty(directory); | ||
internal static void OnProgressChanged(string fileName, byte progressPercentage) { | ||
ArgumentException.ThrowIfNullOrEmpty(fileName); | ||
ArgumentOutOfRangeException.ThrowIfGreaterThan(progressPercentage, 100); | ||
|
||
#pragma warning disable CA1508 // False positive, params could be null when explicitly set | ||
if ((prefixes == null) || (prefixes.Length == 0)) { | ||
#pragma warning restore CA1508 // False positive, params could be null when explicitly set | ||
throw new ArgumentNullException(nameof(prefixes)); | ||
const byte printEveryPercentage = 10; | ||
|
||
if (progressPercentage % printEveryPercentage != 0) { | ||
return; | ||
} | ||
|
||
return (from prefix in prefixes where directory.Length > prefix.Length let pathSeparator = directory[prefix.Length] where (pathSeparator == Path.DirectorySeparatorChar) || (pathSeparator == Path.AltDirectorySeparatorChar) select prefix).Any(prefix => directory.StartsWith(prefix, StringComparison.Ordinal)); | ||
ASF.ArchiLogger.LogGenericDebug($"{fileName} {progressPercentage}%..."); | ||
} | ||
|
||
internal static (bool IsWeak, string? Reason) TestPasswordStrength(string password, ISet<string>? additionallyForbiddenPhrases = null) { | ||
|
@@ -337,6 +322,120 @@ internal static (bool IsWeak, string? Reason) TestPasswordStrength(string passwo | |
return (result.Score < 4, suggestions is { Count: > 0 } ? string.Join(' ', suggestions.Where(static suggestion => suggestion.Length > 0)) : null); | ||
} | ||
|
||
internal static bool UpdateFromArchive(ZipArchive zipArchive, string targetDirectory) { | ||
ArgumentNullException.ThrowIfNull(zipArchive); | ||
ArgumentException.ThrowIfNullOrEmpty(targetDirectory); | ||
|
||
// Firstly we'll move all our existing files to a backup directory | ||
string backupDirectory = Path.Combine(targetDirectory, SharedInfo.UpdateDirectory); | ||
|
||
foreach (string file in Directory.EnumerateFiles(targetDirectory, "*", SearchOption.AllDirectories)) { | ||
string fileName = Path.GetFileName(file); | ||
|
||
if (string.IsNullOrEmpty(fileName)) { | ||
ASF.ArchiLogger.LogNullError(fileName); | ||
|
||
return false; | ||
} | ||
|
||
string relativeFilePath = Path.GetRelativePath(targetDirectory, file); | ||
|
||
if (string.IsNullOrEmpty(relativeFilePath)) { | ||
ASF.ArchiLogger.LogNullError(relativeFilePath); | ||
|
||
return false; | ||
} | ||
|
||
string? relativeDirectoryName = Path.GetDirectoryName(relativeFilePath); | ||
|
||
switch (relativeDirectoryName) { | ||
case null: | ||
ASF.ArchiLogger.LogNullError(relativeDirectoryName); | ||
|
||
return false; | ||
case "": | ||
// No directory, root folder | ||
switch (fileName) { | ||
case Logging.NLogConfigurationFile: | ||
case SharedInfo.LogFile: | ||
// Files with those names in root directory we want to keep | ||
continue; | ||
} | ||
|
||
break; | ||
case SharedInfo.ArchivalLogsDirectory: | ||
case SharedInfo.ConfigDirectory: | ||
case SharedInfo.DebugDirectory: | ||
case SharedInfo.PluginsDirectory: | ||
case SharedInfo.UpdateDirectory: | ||
// Files in those directories we want to keep in their current place | ||
continue; | ||
default: | ||
// Files in subdirectories of those directories we want to keep as well | ||
if (RelativeDirectoryStartsWith(relativeDirectoryName, SharedInfo.ArchivalLogsDirectory, SharedInfo.ConfigDirectory, SharedInfo.DebugDirectory, SharedInfo.PluginsDirectory, SharedInfo.UpdateDirectory)) { | ||
continue; | ||
} | ||
|
||
break; | ||
} | ||
|
||
string targetBackupDirectory = relativeDirectoryName.Length > 0 ? Path.Combine(backupDirectory, relativeDirectoryName) : backupDirectory; | ||
Directory.CreateDirectory(targetBackupDirectory); | ||
|
||
string targetBackupFile = Path.Combine(targetBackupDirectory, fileName); | ||
|
||
File.Move(file, targetBackupFile, true); | ||
} | ||
|
||
// We can now get rid of directories that are empty | ||
DeleteEmptyDirectoriesRecursively(targetDirectory); | ||
|
||
if (!Directory.Exists(targetDirectory)) { | ||
Directory.CreateDirectory(targetDirectory); | ||
} | ||
|
||
// Now enumerate over files in the zip archive, skip directory entries that we're not interested in (we can create them ourselves if needed) | ||
foreach (ZipArchiveEntry zipFile in zipArchive.Entries.Where(static zipFile => !string.IsNullOrEmpty(zipFile.Name))) { | ||
string file = Path.GetFullPath(Path.Combine(targetDirectory, zipFile.FullName)); | ||
|
||
if (!file.StartsWith(targetDirectory, StringComparison.Ordinal)) { | ||
throw new InvalidOperationException(nameof(file)); | ||
} | ||
|
||
if (File.Exists(file)) { | ||
// This is possible only with files that we decided to leave in place during our backup function | ||
string targetBackupFile = $"{file}.bak"; | ||
|
||
File.Move(file, targetBackupFile, true); | ||
} | ||
|
||
// Check if this file requires its own folder | ||
if (zipFile.Name != zipFile.FullName) { | ||
string? directory = Path.GetDirectoryName(file); | ||
|
||
if (string.IsNullOrEmpty(directory)) { | ||
ASF.ArchiLogger.LogNullError(directory); | ||
|
||
return false; | ||
} | ||
|
||
if (!Directory.Exists(directory)) { | ||
Directory.CreateDirectory(directory); | ||
} | ||
|
||
// We're not interested in extracting placeholder files (but we still want directories created for them, done above) | ||
switch (zipFile.Name) { | ||
case ".gitkeep": | ||
continue; | ||
} | ||
} | ||
|
||
zipFile.ExtractToFile(file); | ||
} | ||
|
||
return true; | ||
} | ||
|
||
internal static void WarnAboutIncompleteTranslation(ResourceManager resourceManager) { | ||
ArgumentNullException.ThrowIfNull(resourceManager); | ||
|
||
|
@@ -391,4 +490,38 @@ internal static void WarnAboutIncompleteTranslation(ResourceManager resourceMana | |
ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.TranslationIncomplete, $"{CultureInfo.CurrentUICulture.Name} ({CultureInfo.CurrentUICulture.EnglishName})", translationCompleteness.ToString("P1", CultureInfo.CurrentCulture))); | ||
} | ||
} | ||
|
||
private static void DeleteEmptyDirectoriesRecursively(string directory) { | ||
ArgumentException.ThrowIfNullOrEmpty(directory); | ||
|
||
if (!Directory.Exists(directory)) { | ||
return; | ||
} | ||
|
||
try { | ||
foreach (string subDirectory in Directory.EnumerateDirectories(directory)) { | ||
DeleteEmptyDirectoriesRecursively(subDirectory); | ||
} | ||
|
||
if (!Directory.EnumerateFileSystemEntries(directory).Any()) { | ||
Directory.Delete(directory); | ||
} | ||
} catch (Exception e) { | ||
ASF.ArchiLogger.LogGenericException(e); | ||
} | ||
} | ||
|
||
private static bool RelativeDirectoryStartsWith(string directory, params string[] prefixes) { | ||
ArgumentException.ThrowIfNullOrEmpty(directory); | ||
|
||
#pragma warning disable CA1508 // False positive, params could be null when explicitly set | ||
if ((prefixes == null) || (prefixes.Length == 0)) { | ||
#pragma warning restore CA1508 // False positive, params could be null when explicitly set | ||
throw new ArgumentNullException(nameof(prefixes)); | ||
} | ||
|
||
HashSet<char> separators = [Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar]; | ||
|
||
return prefixes.Where(prefix => (directory.Length > prefix.Length) && separators.Contains(directory[prefix.Length])).Any(prefix => directory.StartsWith(prefix, StringComparison.Ordinal)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,18 +1,20 @@ | ||
// ---------------------------------------------------------------------------------------------- | ||
// _ _ _ ____ _ _____ | ||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ | ||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ | ||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | | ||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| | ||
// | | ||
// ---------------------------------------------------------------------------------------------- | ||
// | ||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki | ||
// Contact: [email protected] | ||
// | | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
|
@@ -172,8 +174,7 @@ private static void ConfigureApp([SuppressMessage("ReSharper", "SuggestBaseTypeF | |
string? assemblyDirectory = Path.GetDirectoryName(plugin.GetType().Assembly.Location); | ||
|
||
if (string.IsNullOrEmpty(assemblyDirectory)) { | ||
// Invalid path provided | ||
continue; | ||
throw new InvalidOperationException(nameof(assemblyDirectory)); | ||
} | ||
|
||
physicalPath = Path.Combine(assemblyDirectory, plugin.PhysicalPath); | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,18 +1,20 @@ | ||
// ---------------------------------------------------------------------------------------------- | ||
// _ _ _ ____ _ _____ | ||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ | ||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ | ||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | | ||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| | ||
// | | ||
// ---------------------------------------------------------------------------------------------- | ||
// | ||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki | ||
// Contact: [email protected] | ||
// | | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
|
@@ -29,6 +31,8 @@ | |
using ArchiSteamFarm.IPC.Responses; | ||
using ArchiSteamFarm.Localization; | ||
using ArchiSteamFarm.Web; | ||
using ArchiSteamFarm.Web.GitHub; | ||
using ArchiSteamFarm.Web.GitHub.Data; | ||
using Microsoft.AspNetCore.Mvc; | ||
|
||
namespace ArchiSteamFarm.IPC.Controllers.Api; | ||
|
@@ -47,7 +51,7 @@ public sealed class GitHubController : ArchiController { | |
public async Task<ActionResult<GenericResponse>> GitHubReleaseGet() { | ||
CancellationToken cancellationToken = HttpContext.RequestAborted; | ||
|
||
GitHub.ReleaseResponse? releaseResponse = await GitHub.GetLatestRelease(false, cancellationToken).ConfigureAwait(false); | ||
ReleaseResponse? releaseResponse = await GitHubService.GetLatestRelease(SharedInfo.GithubRepo, false, cancellationToken).ConfigureAwait(false); | ||
|
||
return releaseResponse != null ? Ok(new GenericResponse<GitHubReleaseResponse>(new GitHubReleaseResponse(releaseResponse))) : StatusCode((int) HttpStatusCode.ServiceUnavailable, new GenericResponse(false, string.Format(CultureInfo.CurrentCulture, Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries))); | ||
} | ||
|
@@ -67,19 +71,19 @@ public async Task<ActionResult<GenericResponse>> GitHubReleaseGet(string version | |
|
||
CancellationToken cancellationToken = HttpContext.RequestAborted; | ||
|
||
GitHub.ReleaseResponse? releaseResponse; | ||
ReleaseResponse? releaseResponse; | ||
|
||
switch (version.ToUpperInvariant()) { | ||
case "LATEST": | ||
releaseResponse = await GitHub.GetLatestRelease(cancellationToken: cancellationToken).ConfigureAwait(false); | ||
releaseResponse = await GitHubService.GetLatestRelease(SharedInfo.GithubRepo, cancellationToken: cancellationToken).ConfigureAwait(false); | ||
|
||
break; | ||
default: | ||
if (!Version.TryParse(version, out Version? parsedVersion)) { | ||
return BadRequest(new GenericResponse(false, string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, nameof(version)))); | ||
} | ||
|
||
releaseResponse = await GitHub.GetRelease(parsedVersion.ToString(4), cancellationToken).ConfigureAwait(false); | ||
releaseResponse = await GitHubService.GetRelease(SharedInfo.GithubRepo, parsedVersion.ToString(4), cancellationToken).ConfigureAwait(false); | ||
|
||
break; | ||
} | ||
|
@@ -102,7 +106,7 @@ public async Task<ActionResult<GenericResponse>> GitHubWikiHistoryGet(string pag | |
|
||
CancellationToken cancellationToken = HttpContext.RequestAborted; | ||
|
||
Dictionary<string, DateTime>? revisions = await GitHub.GetWikiHistory(page, cancellationToken).ConfigureAwait(false); | ||
Dictionary<string, DateTime>? revisions = await GitHubService.GetWikiHistory(page, cancellationToken).ConfigureAwait(false); | ||
|
||
return revisions != null ? revisions.Count > 0 ? Ok(new GenericResponse<ImmutableDictionary<string, DateTime>>(revisions.ToImmutableDictionary())) : BadRequest(new GenericResponse(false, string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, nameof(page)))) : StatusCode((int) HttpStatusCode.ServiceUnavailable, new GenericResponse(false, string.Format(CultureInfo.CurrentCulture, Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries))); | ||
} | ||
|
@@ -123,7 +127,7 @@ public async Task<ActionResult<GenericResponse>> GitHubWikiPageGet(string page, | |
|
||
CancellationToken cancellationToken = HttpContext.RequestAborted; | ||
|
||
string? html = await GitHub.GetWikiPage(page, revision, cancellationToken).ConfigureAwait(false); | ||
string? html = await GitHubService.GetWikiPage(page, revision, cancellationToken).ConfigureAwait(false); | ||
|
||
return html switch { | ||
null => StatusCode((int) HttpStatusCode.ServiceUnavailable, new GenericResponse(false, string.Format(CultureInfo.CurrentCulture, Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries))), | ||
|
Oops, something went wrong.