Workshop - Implemented plugin browsing, installation and removal

This commit is contained in:
Robert committed 2024-02-15 22:57:33 +01:00
1 parent d9df443970
commit 21b8112de5
21 files changed
+410 -35

No files matched your search

@@ -15,6 +15,7 @@ public class EntryInstallationHandlerFactory
{
return entryType switch
{
EntryType.Plugin => _container.Resolve<PluginEntryInstallationHandler>(),
EntryType.Profile => _container.Resolve<ProfileEntryInstallationHandler>(),
EntryType.Layout => _container.Resolve<LayoutEntryInstallationHandler>(),
_ => throw new NotSupportedException($"EntryType '{entryType}' is not supported.")
@@ -14,11 +14,12 @@ public class EntryUninstallResult
};
}
public static EntryUninstallResult FromSuccess()
public static EntryUninstallResult FromSuccess(string? message = null)
{
return new EntryUninstallResult
{
IsSuccess = true
IsSuccess = true,
Message = message
};
}
}
@@ -0,0 +1,122 @@
using System.IO.Compression;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Shared.Extensions;
using Artemis.UI.Shared.Utilities;
using Artemis.WebClient.Workshop.Exceptions;
using Artemis.WebClient.Workshop.Models;
using Artemis.WebClient.Workshop.Services;
namespace Artemis.WebClient.Workshop.Handlers.InstallationHandlers;
public class PluginEntryInstallationHandler : IEntryInstallationHandler
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly IWorkshopService _workshopService;
private readonly IPluginManagementService _pluginManagementService;
public PluginEntryInstallationHandler(IHttpClientFactory httpClientFactory, IWorkshopService workshopService, IPluginManagementService pluginManagementService)
{
_httpClientFactory = httpClientFactory;
_workshopService = workshopService;
_pluginManagementService = pluginManagementService;
}
public async Task<EntryInstallResult> InstallAsync(IEntryDetails entry, IRelease release, Progress<StreamProgress> progress, CancellationToken cancellationToken)
{
// Ensure there is an installed entry
InstalledEntry? installedEntry = _workshopService.GetInstalledEntry(entry);
if (installedEntry != null)
{
// If the folder already exists, we're not going to reinstall the plugin since files may be in use, consider our job done
if (installedEntry.GetReleaseDirectory(release).Exists)
return EntryInstallResult.FromSuccess(installedEntry);
}
else
{
// If none exists yet create one
installedEntry = new InstalledEntry(entry, release);
// Don't try to install a new plugin into an existing directory since files may be in use, consider our job screwed
if (installedEntry.GetReleaseDirectory(release).Exists)
return EntryInstallResult.FromFailure("Plugin is new but installation directory is not empty, try restarting Artemis");
}
using MemoryStream stream = new();
// Download the provided release
try
{
HttpClient client = _httpClientFactory.CreateClient(WorkshopConstants.WORKSHOP_CLIENT_NAME);
await client.DownloadDataAsync($"releases/download/{release.Id}", stream, progress, cancellationToken);
}
catch (Exception e)
{
return EntryInstallResult.FromFailure(e.Message);
}
// Create the release directory
DirectoryInfo releaseDirectory = installedEntry.GetReleaseDirectory(release);
releaseDirectory.Create();
// Extract the archive
stream.Seek(0, SeekOrigin.Begin);
using ZipArchive archive = new(stream);
archive.ExtractToDirectory(releaseDirectory.FullName);
// Load the plugin, next time during startup this will happen automatically
try
{
Plugin? plugin = _pluginManagementService.LoadPlugin(releaseDirectory);
if (plugin == null)
throw new ArtemisWorkshopException("Failed to load plugin, it may be incompatible");
installedEntry.SetMetadata("PluginId", plugin.Guid);
}
catch (Exception e)
{
// If the plugin ended up being invalid yoink it out again, shoooo
try
{
releaseDirectory.Delete(true);
}
catch (Exception)
{
// ignored, will get cleaned up as an orphaned file
}
_workshopService.RemoveInstalledEntry(installedEntry);
return EntryInstallResult.FromFailure(e.Message);
}
_workshopService.SaveInstalledEntry(installedEntry);
return EntryInstallResult.FromSuccess(installedEntry);
}
public Task<EntryUninstallResult> UninstallAsync(InstalledEntry installedEntry, CancellationToken cancellationToken)
{
// Disable the plugin
if (installedEntry.TryGetMetadata("PluginId", out Guid pluginId))
{
Plugin? plugin = _pluginManagementService.GetAllPlugins().FirstOrDefault(p => p.Guid == pluginId);
if (plugin != null)
_pluginManagementService.UnloadPlugin(plugin);
}
// Attempt to remove from filesystem
DirectoryInfo directory = installedEntry.GetDirectory();
string? message = null;
try
{
if (directory.Exists)
directory.Delete(true);
}
catch (Exception)
{
message = "Failed to clean up files, you may need to restart Artemis";
}
// Remove entry
_workshopService.RemoveInstalledEntry(installedEntry);
return Task.FromResult(EntryUninstallResult.FromSuccess(message));
}
}
@@ -17,6 +17,7 @@ public interface IWorkshopService
InstalledEntry? GetInstalledEntry(IEntryDetails entry);
void RemoveInstalledEntry(InstalledEntry installedEntry);
void SaveInstalledEntry(InstalledEntry entry);
void RemoveOrphanedFiles();
public record WorkshopStatus(bool IsReachable, string Message);
}
@@ -1,20 +1,24 @@
using System.Net.Http.Headers;
using Artemis.Core;
using Artemis.Storage.Entities.Workshop;
using Artemis.Storage.Repositories.Interfaces;
using Artemis.UI.Shared.Routing;
using Artemis.WebClient.Workshop.Handlers.UploadHandlers;
using Artemis.WebClient.Workshop.Models;
using Serilog;
namespace Artemis.WebClient.Workshop.Services;
public class WorkshopService : IWorkshopService
{
private readonly ILogger _logger;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IRouter _router;
private readonly IEntryRepository _entryRepository;
public WorkshopService(IHttpClientFactory httpClientFactory, IRouter router, IEntryRepository entryRepository)
public WorkshopService(ILogger logger, IHttpClientFactory httpClientFactory, IRouter router, IEntryRepository entryRepository)
{
_logger = logger;
_httpClientFactory = httpClientFactory;
_router = router;
_entryRepository = entryRepository;
@@ -117,6 +121,7 @@ public class WorkshopService : IWorkshopService
return status.IsReachable;
}
/// <inheritdoc />
public async Task NavigateToEntry(long entryId, EntryType entryType)
{
switch (entryType)
@@ -135,6 +140,7 @@ public class WorkshopService : IWorkshopService
}
}
/// <inheritdoc />
public List<InstalledEntry> GetInstalledEntries()
{
return _entryRepository.GetAll().Select(e => new InstalledEntry(e)).ToList();
@@ -150,12 +156,6 @@ public class WorkshopService : IWorkshopService
return new InstalledEntry(entity);
}
/// <inheritdoc />
public void AddOrUpdateInstalledEntry(InstalledEntry entry, IRelease release)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public void RemoveInstalledEntry(InstalledEntry installedEntry)
{
@@ -168,4 +168,38 @@ public class WorkshopService : IWorkshopService
entry.Save();
_entryRepository.Save(entry.Entity);
}
/// <inheritdoc />
public void RemoveOrphanedFiles()
{
List<InstalledEntry> entries = GetInstalledEntries();
foreach (string directory in Directory.GetDirectories(Constants.WorkshopFolder))
{
InstalledEntry? installedEntry = entries.FirstOrDefault(e => e.GetDirectory().FullName == directory);
if (installedEntry == null)
RemoveOrphanedDirectory(directory);
else
{
DirectoryInfo currentReleaseDirectory = installedEntry.GetReleaseDirectory();
foreach (string releaseDirectory in Directory.GetDirectories(directory))
{
if (releaseDirectory != currentReleaseDirectory.FullName)
RemoveOrphanedDirectory(releaseDirectory);
}
}
}
}
private void RemoveOrphanedDirectory(string directory)
{
_logger.Information("Removing orphaned workshop entry at {Directory}", directory);
try
{
Directory.Delete(directory, true);
}
catch (Exception e)
{
_logger.Warning(e, "Failed to remove orphaned workshop entry at {Directory}", directory);
}
}
}
@@ -2,10 +2,10 @@ namespace Artemis.WebClient.Workshop;
public static class WorkshopConstants
{
// public const string AUTHORITY_URL = "https://localhost:5001";
// public const string WORKSHOP_URL = "https://localhost:7281";
public const string AUTHORITY_URL = "https://identity.artemis-rgb.com";
public const string WORKSHOP_URL = "https://workshop.artemis-rgb.com";
public const string AUTHORITY_URL = "https://localhost:5001";
public const string WORKSHOP_URL = "https://localhost:7281";
// public const string AUTHORITY_URL = "https://identity.artemis-rgb.com";
// public const string WORKSHOP_URL = "https://workshop.artemis-rgb.com";
public const string IDENTITY_CLIENT_NAME = "IdentityApiClient";
public const string WORKSHOP_CLIENT_NAME = "WorkshopApiClient";
}