Workshop - Added profile uploading

Submission wizard - Final upload steps
This commit is contained in:
Robert committed 2023-08-13 21:11:05 +02:00
1 parent e52faf1c9f
commit ad4da3032d
32 files changed
+621 -93

No files matched your search

@@ -1,5 +1,9 @@
using System.Reflection;
using Artemis.WebClient.Workshop.Extensions;
using Artemis.WebClient.Workshop.Repositories;
using Artemis.WebClient.Workshop.Services;
using Artemis.WebClient.Workshop.State;
using Artemis.WebClient.Workshop.UploadHandlers;
using DryIoc;
using DryIoc.Microsoft.DependencyInjection;
using IdentityModel.Client;
@@ -18,11 +22,17 @@ public static class ContainerExtensions
/// <param name="container">The builder building the current container</param>
public static void RegisterWorkshopClient(this IContainer container)
{
Assembly[] workshopAssembly = {typeof(WorkshopConstants).Assembly};
ServiceCollection serviceCollection = new();
serviceCollection
.AddHttpClient()
.AddWorkshopClient()
.AddHttpMessageHandler<WorkshopClientStoreAccessor, AuthenticationDelegatingHandler>()
.ConfigureHttpClient(client => client.BaseAddress = new Uri(WorkshopConstants.WORKSHOP_URL + "/graphql"));
serviceCollection.AddHttpClient(WorkshopConstants.WORKSHOP_CLIENT_NAME)
.AddHttpMessageHandler<AuthenticationDelegatingHandler>()
.ConfigureHttpClient(client => client.BaseAddress = new Uri(WorkshopConstants.WORKSHOP_URL));
serviceCollection.AddSingleton<IDiscoveryCache>(r =>
{
@@ -34,5 +44,8 @@ public static class ContainerExtensions
container.Register<IAuthenticationRepository, AuthenticationRepository>(Reuse.Singleton);
container.Register<IAuthenticationService, AuthenticationService>(Reuse.Singleton);
container.Register<EntryUploadHandlerFactory>(Reuse.Transient);
container.RegisterMany(workshopAssembly, type => type.IsAssignableTo<IEntryUploadHandler>(), Reuse.Transient);
}
}
@@ -0,0 +1,22 @@
using System.ComponentModel.DataAnnotations;
namespace Artemis.Web.Workshop.Entities;
public class Release
{
public Guid Id { get; set; }
[MaxLength(64)]
public string Version { get; set; } = string.Empty;
public DateTimeOffset CreatedAt { get; set; }
public long Downloads { get; set; }
public long DownloadSize { get; set; }
[MaxLength(32)]
public string? Md5Hash { get; set; }
public Guid EntryId { get; set; }
}
@@ -0,0 +1,18 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http;
using StrawberryShake;
namespace Artemis.WebClient.Workshop.Extensions;
public static class ClientBuilderExtensions
{
public static IClientBuilder<T> AddHttpMessageHandler<T, THandler>(this IClientBuilder<T> builder) where THandler : DelegatingHandler where T : IStoreAccessor
{
builder.Services.Configure<HttpClientFactoryOptions>(
builder.ClientName,
options => options.HttpMessageHandlerBuilderActions.Add(b => b.AdditionalHandlers.Add(b.Services.GetRequiredService<THandler>()))
);
return builder;
}
}
@@ -0,0 +1,5 @@
mutation AddEntry ($input: CreateEntryInput!) {
addEntry(input: $input) {
id
}
}
@@ -0,0 +1,22 @@
using System.Net.Http.Headers;
namespace Artemis.WebClient.Workshop.Services;
public class AuthenticationDelegatingHandler : DelegatingHandler
{
private readonly IAuthenticationService _authenticationService;
/// <inheritdoc />
public AuthenticationDelegatingHandler(IAuthenticationService authenticationService)
{
_authenticationService = authenticationService;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
string? token = await _authenticationService.GetBearer();
if (token != null)
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
return await base.SendAsync(request, cancellationToken);
}
}
@@ -0,0 +1,24 @@
using Artemis.WebClient.Workshop.UploadHandlers.Implementations;
using DryIoc;
namespace Artemis.WebClient.Workshop.UploadHandlers;
public class EntryUploadHandlerFactory
{
private readonly IContainer _container;
public EntryUploadHandlerFactory(IContainer container)
{
_container = container;
}
public IEntryUploadHandler CreateHandler(EntryType entryType)
{
return entryType switch
{
EntryType.Profile => _container.Resolve<ProfileEntryUploadHandler>(),
EntryType.Layout => _container.Resolve<LayoutEntryUploadHandler>(),
_ => throw new NotSupportedException($"EntryType '{entryType}' is not supported.")
};
}
}
@@ -0,0 +1,28 @@
using Artemis.Web.Workshop.Entities;
namespace Artemis.WebClient.Workshop.UploadHandlers;
public class EntryUploadResult
{
public bool IsSuccess { get; set; }
public string? Message { get; set; }
public Release? Release { get; set; }
public static EntryUploadResult FromFailure(string? message)
{
return new EntryUploadResult
{
IsSuccess = false,
Message = message
};
}
public static EntryUploadResult FromSuccess(Release release)
{
return new EntryUploadResult
{
IsSuccess = true,
Release = release
};
}
}
@@ -0,0 +1,7 @@
namespace Artemis.WebClient.Workshop.UploadHandlers;
public interface IEntryUploadHandler
{
Task<EntryUploadResult> CreateReleaseAsync(Guid entryId, object file, CancellationToken cancellationToken);
}
@@ -0,0 +1,12 @@
using RGB.NET.Layout;
namespace Artemis.WebClient.Workshop.UploadHandlers.Implementations;
public class LayoutEntryUploadHandler : IEntryUploadHandler
{
/// <inheritdoc />
public async Task<EntryUploadResult> CreateReleaseAsync(Guid entryId, object file, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
}
@@ -0,0 +1,61 @@
using System.IO.Compression;
using System.Net.Http.Headers;
using System.Text;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.Web.Workshop.Entities;
using Newtonsoft.Json;
namespace Artemis.WebClient.Workshop.UploadHandlers.Implementations;
public class ProfileEntryUploadHandler : IEntryUploadHandler
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly IProfileService _profileService;
public ProfileEntryUploadHandler(IHttpClientFactory httpClientFactory, IProfileService profileService)
{
_httpClientFactory = httpClientFactory;
_profileService = profileService;
}
/// <inheritdoc />
public async Task<EntryUploadResult> CreateReleaseAsync(Guid entryId, object file, CancellationToken cancellationToken)
{
if (file is not ProfileConfiguration profileConfiguration)
throw new InvalidOperationException("Can only create releases for profile configurations");
ProfileConfigurationExportModel export = _profileService.ExportProfile(profileConfiguration);
string json = JsonConvert.SerializeObject(export, IProfileService.ExportSettings);
using MemoryStream archiveStream = new();
// Create a ZIP archive with a single entry on the archive stream
using (ZipArchive archive = new(archiveStream, ZipArchiveMode.Create, true))
{
ZipArchiveEntry entry = archive.CreateEntry("profile.json");
await using (Stream entryStream = entry.Open())
{
await entryStream.WriteAsync(Encoding.Default.GetBytes(json), cancellationToken);
}
}
// Submit the archive
HttpClient client = _httpClientFactory.CreateClient(WorkshopConstants.WORKSHOP_CLIENT_NAME);
// Construct the request
archiveStream.Seek(0, SeekOrigin.Begin);
MultipartFormDataContent content = new();
StreamContent streamContent = new(archiveStream);
streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/zip");
content.Add(streamContent, "file", "file.zip");
// Submit
HttpResponseMessage response = await client.PostAsync("releases/upload/" + entryId, content, cancellationToken);
if (!response.IsSuccessStatusCode)
return EntryUploadResult.FromFailure($"{response.StatusCode} - {await response.Content.ReadAsStringAsync(cancellationToken)}");
Release? release = JsonConvert.DeserializeObject<Release>(await response.Content.ReadAsStringAsync(cancellationToken));
return release != null ? EntryUploadResult.FromSuccess(release) : EntryUploadResult.FromFailure("Failed to deserialize response");
}
}
@@ -4,4 +4,5 @@ public static class WorkshopConstants
{
public const string AUTHORITY_URL = "https://identity.artemis-rgb.com";
public const string WORKSHOP_URL = "https://workshop.artemis-rgb.com";
public const string WORKSHOP_CLIENT_NAME = "WorkshopApiClient";
}