Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement plugin updates with IPluginUpdates interface #3151

Merged
merged 33 commits into from
Mar 16, 2024
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
9e0ca8f
Initial implementation of plugin updates
JustArchi Mar 9, 2024
733fb82
Update PluginsCore.cs
JustArchi Mar 9, 2024
c746317
Update IPluginUpdates.cs
JustArchi Mar 9, 2024
8fa277d
Update PluginsCore.cs
JustArchi Mar 9, 2024
d30dcb5
Make it work
JustArchi Mar 9, 2024
bccd1bb
Misc
JustArchi Mar 9, 2024
f1b5a8d
Revert "Misc"
JustArchi Mar 9, 2024
008cd6c
Proper fix
JustArchi Mar 9, 2024
bd00a72
Make plugin updates independent of GitHub
JustArchi Mar 10, 2024
3502f25
Merge branch 'main' into feature/plugin-updates
JustArchi Mar 11, 2024
4674db8
Final touches
JustArchi Mar 11, 2024
55ac3af
Misc
JustArchi Mar 12, 2024
cf1753b
Allow plugin creators for more flexibility in picking from GitHub rel…
JustArchi Mar 12, 2024
af19bcd
Misc rename
JustArchi Mar 12, 2024
4b3810e
Make changelog internal again
JustArchi Mar 12, 2024
44b4f1a
Misc
JustArchi Mar 12, 2024
1cb0a2d
Add missing localization
JustArchi Mar 12, 2024
bba7b53
Add a way to disable plugin updates
JustArchi Mar 12, 2024
e728ca8
Update PluginsCore.cs
JustArchi Mar 12, 2024
2a3b41a
Update PluginsCore.cs
JustArchi Mar 12, 2024
f0f4be4
Misc
JustArchi Mar 12, 2024
4743dc0
Update IGitHubPluginUpdates.cs
JustArchi Mar 12, 2024
dbd00fe
Update IGitHubPluginUpdates.cs
JustArchi Mar 12, 2024
6e4695f
Update IGitHubPluginUpdates.cs
JustArchi Mar 12, 2024
e1d4f7b
Update IGitHubPluginUpdates.cs
JustArchi Mar 12, 2024
8976d25
Make zip selection ignore case
JustArchi Mar 12, 2024
043858a
Update ArchiSteamFarm/Core/Utilities.cs
JustArchi Mar 12, 2024
da5e56e
Misc error notify
JustArchi Mar 12, 2024
a22b230
Add commands and finally call it a day
JustArchi Mar 13, 2024
62f52f7
Misc progress percentages text
JustArchi Mar 15, 2024
f6cbe31
Misc
JustArchi Mar 15, 2024
e78ff22
Flip DefaultPluginsUpdateMode as per the voting
JustArchi Mar 16, 2024
8764a14
Misc
JustArchi Mar 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
525 changes: 217 additions & 308 deletions ArchiSteamFarm/Core/ASF.cs

Large diffs are not rendered by default.

192 changes: 161 additions & 31 deletions ArchiSteamFarm/Core/Utilities.cs
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.
Expand All @@ -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;
Expand All @@ -35,6 +38,7 @@
using AngleSharp.Dom;
using AngleSharp.XPath;
using ArchiSteamFarm.Localization;
using ArchiSteamFarm.NLog;
using ArchiSteamFarm.Storage;
using Humanizer;
using Humanizer.Localisation;
Expand Down Expand Up @@ -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;
Expand All @@ -291,16 +275,14 @@ 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(object? sender, byte progressPercentage) {
const byte printEveryPercentage = 10;

#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));
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($"{progressPercentage}%...");
JustArchi marked this conversation as resolved.
Show resolved Hide resolved
}

internal static (bool IsWeak, string? Reason) TestPasswordStrength(string password, ISet<string>? additionallyForbiddenPhrases = null) {
Expand Down Expand Up @@ -337,6 +319,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);

Expand Down Expand Up @@ -391,4 +487,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));
}
}
13 changes: 7 additions & 6 deletions ArchiSteamFarm/IPC/ArchiKestrel.cs
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.
Expand Down Expand Up @@ -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);
Expand Down
24 changes: 14 additions & 10 deletions ArchiSteamFarm/IPC/Controllers/Api/GitHubController.cs
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.
Expand All @@ -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;
Expand All @@ -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)));
}
Expand All @@ -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;
}
Expand All @@ -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)));
}
Expand All @@ -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))),
Expand Down
Loading
Loading