using System.Net.Http.Headers; using Artemis.Core; using Artemis.Core.Services; using Artemis.Storage.Entities.Workshop; using Artemis.Storage.Repositories.Interfaces; using Artemis.UI.Shared.Routing; using Artemis.UI.Shared.Utilities; using Artemis.WebClient.Workshop.Exceptions; using Artemis.WebClient.Workshop.Handlers.InstallationHandlers; 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; private readonly Lazy _pluginManagementService; private readonly Lazy _profileService; private readonly EntryInstallationHandlerFactory _factory; private readonly IPluginRepository _pluginRepository; private readonly IWorkshopClient _workshopClient; private readonly PluginSetting _migratedBuiltInPlugins; private bool _initialized; private bool _mutating; public WorkshopService(ILogger logger, IHttpClientFactory httpClientFactory, IRouter router, IEntryRepository entryRepository, ISettingsService settingsService, Lazy pluginManagementService, Lazy profileService, EntryInstallationHandlerFactory factory, IPluginRepository pluginRepository, IWorkshopClient workshopClient) { _logger = logger; _httpClientFactory = httpClientFactory; _router = router; _entryRepository = entryRepository; _pluginManagementService = pluginManagementService; _profileService = profileService; _factory = factory; _pluginRepository = pluginRepository; _workshopClient = workshopClient; _migratedBuiltInPlugins = settingsService.GetSetting("Workshop.MigratedBuiltInPlugins", false); } public async Task GetEntryIcon(long entryId, CancellationToken cancellationToken) { HttpClient client = _httpClientFactory.CreateClient(WorkshopConstants.WORKSHOP_CLIENT_NAME); try { HttpResponseMessage response = await client.GetAsync($"entries/{entryId}/icon", cancellationToken); response.EnsureSuccessStatusCode(); return await response.Content.ReadAsStreamAsync(cancellationToken); } catch (HttpRequestException) { // ignored return null; } } public async Task SetEntryIcon(long entryId, Stream icon, CancellationToken cancellationToken) { icon.Seek(0, SeekOrigin.Begin); // Submit the archive HttpClient client = _httpClientFactory.CreateClient(WorkshopConstants.WORKSHOP_CLIENT_NAME); // Construct the request MultipartFormDataContent content = new(); StreamContent streamContent = new(icon); streamContent.Headers.ContentType = new MediaTypeHeaderValue("image/png"); content.Add(streamContent, "file", "file.png"); // Submit HttpResponseMessage response = await client.PostAsync($"entries/{entryId}/icon", content, cancellationToken); if (!response.IsSuccessStatusCode) return ApiResult.FromFailure($"{response.StatusCode} - {await response.Content.ReadAsStringAsync(cancellationToken)}"); return ApiResult.FromSuccess(); } /// public async Task UploadEntryImage(long entryId, ImageUploadRequest request, CancellationToken cancellationToken) { request.File.Seek(0, SeekOrigin.Begin); // Submit the archive HttpClient client = _httpClientFactory.CreateClient(WorkshopConstants.WORKSHOP_CLIENT_NAME); // Construct the request MultipartFormDataContent content = new(); StreamContent streamContent = new(request.File); streamContent.Headers.ContentType = new MediaTypeHeaderValue("image/png"); content.Add(streamContent, "file", "file.png"); content.Add(new StringContent(request.Name), "Name"); if (request.Description != null) content.Add(new StringContent(request.Description), "Description"); // Submit HttpResponseMessage response = await client.PostAsync($"entries/{entryId}/image", content, cancellationToken); if (!response.IsSuccessStatusCode) return ApiResult.FromFailure($"{response.StatusCode} - {await response.Content.ReadAsStringAsync(cancellationToken)}"); return ApiResult.FromSuccess(); } /// public async Task DeleteEntryImage(Guid id, CancellationToken cancellationToken) { HttpClient client = _httpClientFactory.CreateClient(WorkshopConstants.WORKSHOP_CLIENT_NAME); HttpResponseMessage response = await client.DeleteAsync($"images/manage/{id}", cancellationToken); response.EnsureSuccessStatusCode(); } /// public async Task GetWorkshopStatus(CancellationToken cancellationToken) { try { // Don't use the workshop client which adds auth headers HttpClient client = _httpClientFactory.CreateClient(); HttpResponseMessage response = await client.SendAsync(new HttpRequestMessage(HttpMethod.Head, WorkshopConstants.WORKSHOP_URL + "/status"), cancellationToken); return new IWorkshopService.WorkshopStatus(response.IsSuccessStatusCode, response.StatusCode.ToString()); } catch (OperationCanceledException e) { return new IWorkshopService.WorkshopStatus(false, e.Message); } catch (HttpRequestException e) { return new IWorkshopService.WorkshopStatus(false, e.Message); } } /// public async Task ValidateWorkshopStatus(bool navigateIfUnreachable, CancellationToken cancellationToken) { IWorkshopService.WorkshopStatus status = await GetWorkshopStatus(cancellationToken); if (navigateIfUnreachable && !status.IsReachable && !cancellationToken.IsCancellationRequested) await _router.Navigate($"workshop/offline/{status.Message}"); return status.IsReachable; } /// public async Task NavigateToEntry(long entryId, EntryType entryType) { switch (entryType) { case EntryType.Profile: await _router.Navigate($"workshop/entries/profiles/details/{entryId}"); break; case EntryType.Layout: await _router.Navigate($"workshop/entries/layouts/details/{entryId}"); break; case EntryType.Plugin: await _router.Navigate($"workshop/entries/plugins/details/{entryId}"); break; default: throw new ArgumentOutOfRangeException(nameof(entryType)); } } /// public async Task InstallEntry(IEntrySummary entry, IRelease release, Progress progress, CancellationToken cancellationToken) { _mutating = true; try { IEntryInstallationHandler handler = _factory.CreateHandler(entry.EntryType); EntryInstallResult result = await handler.InstallAsync(entry, release, progress, cancellationToken); if (result.IsSuccess && result.Entry != null) OnEntryInstalled?.Invoke(this, result.Entry); else _logger.Warning("Failed to install entry {Entry}: {Message}", entry, result.Message); return result; } finally { _mutating = false; } } /// public async Task UninstallEntry(InstalledEntry installedEntry, CancellationToken cancellationToken) { _mutating = true; try { IEntryInstallationHandler handler = _factory.CreateHandler(installedEntry.EntryType); EntryUninstallResult result = await handler.UninstallAsync(installedEntry, cancellationToken); if (result.IsSuccess) OnEntryUninstalled?.Invoke(this, installedEntry); else _logger.Warning("Failed to uninstall entry {EntryId}: {Message}", installedEntry.Id, result.Message); return result; } finally { _mutating = false; } } /// public List GetInstalledEntries() { return _entryRepository.GetAll().Select(e => new InstalledEntry(e)).ToList(); } /// public InstalledEntry? GetInstalledEntry(long entryId) { EntryEntity? entity = _entryRepository.GetByEntryId(entryId); if (entity == null) return null; return new InstalledEntry(entity); } /// public InstalledEntry? GetInstalledEntryByPlugin(Plugin plugin) { return GetInstalledEntries().FirstOrDefault(e => e.TryGetMetadata("PluginId", out Guid pluginId) && pluginId == plugin.Guid); } /// public InstalledEntry? GetInstalledEntryByProfile(ProfileConfiguration profileConfiguration) { return GetInstalledEntries().FirstOrDefault(e => e.TryGetMetadata("ProfileId", out Guid pluginId) && pluginId == profileConfiguration.ProfileId); } /// public void RemoveInstalledEntry(InstalledEntry installedEntry) { _entryRepository.Remove(installedEntry.Entity); } /// public void SaveInstalledEntry(InstalledEntry entry) { entry.Save(); _entryRepository.Save(entry.Entity); OnInstalledEntrySaved?.Invoke(this, entry); } /// public async Task Initialize() { if (_initialized) throw new ArtemisWorkshopException("Workshop service is already initialized"); try { if (!Directory.Exists(Constants.WorkshopFolder)) Directory.CreateDirectory(Constants.WorkshopFolder); RemoveOrphanedFiles(); await MigrateBuiltInPlugins(); _pluginManagementService.Value.AdditionalPluginDirectories.AddRange(GetInstalledEntries() .Where(e => e.EntryType == EntryType.Plugin) .Select(e => e.GetReleaseDirectory())); _pluginManagementService.Value.PluginRemoved += PluginManagementServiceOnPluginRemoved; _profileService.Value.ProfileRemoved += ProfileServiceOnProfileRemoved; _initialized = true; } catch (Exception e) { _logger.Error(e, "Failed to initialize workshop service"); } } /// public void SetAutoUpdate(InstalledEntry installedEntry, bool autoUpdate) { if (installedEntry.AutoUpdate == autoUpdate) return; installedEntry.AutoUpdate = autoUpdate; SaveInstalledEntry(installedEntry); } private void RemoveOrphanedFiles() { List 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); } } private async Task MigrateBuiltInPlugins() { // If already migrated, do nothing if (_migratedBuiltInPlugins.Value) return; _mutating = true; try { MigratingBuildInPlugins?.Invoke(this, EventArgs.Empty); bool migrated = await BuiltInPluginsMigrator.Migrate(this, _workshopClient, _logger, _pluginRepository); _migratedBuiltInPlugins.Value = migrated; _migratedBuiltInPlugins.Save(); } finally { _mutating = false; } } private void ProfileServiceOnProfileRemoved(object? sender, ProfileConfigurationEventArgs e) { if (_mutating) return; InstalledEntry? entry = GetInstalledEntryByProfile(e.ProfileConfiguration); if (entry == null) return; _logger.Information("Profile {Profile} was removed, uninstalling entry", e.ProfileConfiguration); Task.Run(() => UninstallEntry(entry, CancellationToken.None)); } private void PluginManagementServiceOnPluginRemoved(object? sender, PluginEventArgs e) { if (_mutating) return; InstalledEntry? entry = GetInstalledEntryByPlugin(e.Plugin); if (entry == null) return; _logger.Information("Plugin {Plugin} was removed, uninstalling entry", e.Plugin); Task.Run(() => UninstallEntry(entry, CancellationToken.None)); } public event EventHandler? OnInstalledEntrySaved; public event EventHandler? OnEntryUninstalled; public event EventHandler? OnEntryInstalled; public event EventHandler? MigratingBuildInPlugins; }