Skip to content
Merged

Deploy #1758

Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion _magic/k8s-deployment-web.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ metadata:
labels:
app: wowthing-again-web
spec:
replicas: 2
replicas: 4
selector:
matchLabels:
app: wowthing-again-web
Expand Down
1 change: 1 addition & 0 deletions apps/backend/Jobs/JobBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public abstract class JobBase : IJob, IDisposable
internal JobRepository JobRepository;
internal JsonSerializerOptions JsonSerializerOptions;
internal MemoryCacheService MemoryCacheService;
internal S3Service S3Service;
internal StateService StateService;

internal long UserId;
Expand Down
4 changes: 4 additions & 0 deletions apps/backend/Jobs/JobFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public class JobFactory
private readonly JobRepository _jobRepository;
private readonly JsonSerializerOptions _jsonSerializerOptions;
private readonly MemoryCacheService _memoryCacheService;
private readonly S3Service _s3Service;
private readonly StateService _stateService;

private readonly ConnectionMultiplexer _redis;
Expand All @@ -31,6 +32,7 @@ public JobFactory(
JobRepository jobRepository,
JsonSerializerOptions jsonSerializerOptions,
MemoryCacheService memoryCacheService,
S3Service s3Service,
StateService stateService,
string redisConnectionString)
{
Expand All @@ -42,6 +44,7 @@ public JobFactory(
_jsonSerializerOptions = jsonSerializerOptions;
_logger = logger;
_memoryCacheService = memoryCacheService;
_s3Service = s3Service;
_stateService = stateService;

_redis = RedisUtilities.GetConnection(redisConnectionString);
Expand All @@ -56,6 +59,7 @@ public JobBase Create(Type type, IDbContextFactory<WowDbContext> contextFactory,
obj.JsonSerializerOptions = _jsonSerializerOptions;
obj.Logger = _logger;
obj.Redis = _redis;
obj.S3Service = _s3Service;
obj.StateService = _stateService;

obj.CancellationToken = cancellationToken;
Expand Down
50 changes: 39 additions & 11 deletions apps/backend/Jobs/Misc/ImageJob.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ public override async Task Run(string[] data)
return;
}

bool useS3 = S3Service.IsEnabled;
string sha256 = result.Data.Sha256();

var image = await Context.Image.FindAsync(type, id, format);
if (image == null)
{
Expand All @@ -34,6 +37,27 @@ public override async Task Run(string[] data)
};
Context.Image.Add(image);
}
else if (image.Sha256 == sha256 && ((useS3 && image.Data == null) || (!useS3 && image.Data != null)))
{
Logger.Debug("Hash matches");
return;
}
else if (useS3)
{
// Hash is changing, delete the old file if a single row references it
int rowCount = await Context.Image
.Where(img => img.Type == image.Type &&
img.Sha256 == image.Sha256 &&
img.Format == image.Format)
.CountAsync(CancellationToken);
if (rowCount == 1)
{
await S3Service.DeleteImageAsync(image, CancellationToken);
timer.AddPoint("S3Delete");
}
}

image.Sha256 = sha256;

if (
(format == ImageFormat.Jpeg && (url.EndsWith(".jpg") || url.EndsWith(".jpeg"))) ||
Expand All @@ -55,14 +79,12 @@ public override async Task Run(string[] data)
.InProcessAsync();

var convertedBytes = converted.First.TryGetBytes();
if (convertedBytes.HasValue)
{
image.Data = convertedBytes.Value.ToArray();
}
else
if (!convertedBytes.HasValue)
{
throw new Exception($"Image conversion failed: type={type} id={id} format={format} url={url}");
}

image.Data = convertedBytes.Value.ToArray();
}
}
else
Expand All @@ -72,15 +94,21 @@ public override async Task Run(string[] data)

timer.AddPoint("Convert");

var sha256 = image.Data.Sha256();
if (sha256 == image.Sha256)
// Upload the file instead if we're using S3
if (useS3)
{
Logger.Debug("Hash matches");
return;
bool uploadOk = await S3Service.UploadImageAsync(image, CancellationToken);
if (uploadOk)
{
image.Data = null;
}
else
{
Logger.Warning("Upload failed: {path}", image.S3Path);
}
timer.AddPoint("S3Upload");
}

image.Sha256 = sha256;

await Context.SaveChangesAsync(CancellationToken);

timer.AddPoint("Save", true);
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/Models/BattleNetOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ public class BattleNetOptions
{
public string ClientId { get; set; }
public string ClientSecret { get; set; }
}
}
6 changes: 5 additions & 1 deletion apps/backend/Models/WowthingBackendOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@

public class WowthingBackendOptions
{
public List<string> AllAuctionRegions { get; set; }
public int ApiRateLimit { get; set; }
public int WorkerMaxAuction { get; set; }
public int WorkerMaxBulk { get; set; }
public int WorkerMaxHigh { get; set; }
public int WorkerMaxLow { get; set; }
public string ImageAccessKeyId { get; set; }
public string ImageSecretAccessKey { get; set; }
public string ImageEndpoint { get; set; }
public string ImageBucket { get; set; }
public List<string> AllAuctionRegions { get; set; }
}
1 change: 1 addition & 0 deletions apps/backend/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ private static void ConfigureServices(HostBuilderContext hostContext, IServiceCo
// Services
services.AddSingleton<CacheService>();
services.AddSingleton<MemoryCacheService>();
services.AddSingleton<S3Service>();
services.AddSingleton<StateService>();

services.AddHostedService<AuthorizationService>();
Expand Down
76 changes: 76 additions & 0 deletions apps/backend/Services/S3Service.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using Amazon.Runtime;
using Amazon.S3;
using Amazon.S3.Model;
using Microsoft.Extensions.Options;
using Serilog;
using Wowthing.Backend.Models;
using Wowthing.Lib.Models;

namespace Wowthing.Backend.Services;

public class S3Service
{
private readonly ILogger _logger;
private readonly WowthingBackendOptions _backendOptions;

public S3Service(IOptions<WowthingBackendOptions> backendOptions)
{
_backendOptions = backendOptions.Value;

_logger = Log.ForContext("Service", $"S3Service");

_logger.Information("Service starting");

_logger.Debug("ImageAccessKeyId {0}", _backendOptions.ImageAccessKeyId);
_logger.Debug("ImageSecretAccessKey {0}", _backendOptions.ImageSecretAccessKey);
_logger.Debug("ImageEndpoint {0}", _backendOptions.ImageEndpoint);
_logger.Debug("ImageBucket {0}", _backendOptions.ImageBucket);
}

private AmazonS3Client S3Client
{
get
{
if (field == null)
{
var credentials = new BasicAWSCredentials(_backendOptions.ImageAccessKeyId, _backendOptions.ImageSecretAccessKey);
field = new AmazonS3Client(credentials, new AmazonS3Config
{
ServiceURL = _backendOptions.ImageEndpoint
});
}
return field;
}
}

public bool IsEnabled => !string.IsNullOrEmpty(_backendOptions.ImageBucket);

public async Task<bool> UploadImageAsync(Image image, CancellationToken cancellationToken)
{
using var memoryStream = new MemoryStream(image.Data);

var request = new PutObjectRequest
{
BucketName = _backendOptions.ImageBucket,
Key = image.S3Path,
InputStream = memoryStream,
ContentType = $"image/{image.Format.ToString().ToLower()}",
// Cloudflare R2 doesn't support SigV4
DisableDefaultChecksumValidation = true,
DisablePayloadSigning = true,
};
var response = await S3Client.PutObjectAsync(request, cancellationToken);
return response.HttpStatusCode == System.Net.HttpStatusCode.OK;
}

public async Task<bool> DeleteImageAsync(Image image, CancellationToken cancellationToken)
{
var request = new DeleteObjectRequest
{
BucketName = _backendOptions.ImageBucket,
Key = image.S3Path,
};
var response = await S3Client.DeleteObjectAsync(request, cancellationToken);
return response.HttpStatusCode == System.Net.HttpStatusCode.OK;
}
}
2 changes: 2 additions & 0 deletions apps/backend/Services/WorkerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public WorkerService(
JobRepository jobRepository,
JsonSerializerOptions jsonSerializerOptions,
MemoryCacheService memoryCacheService,
S3Service s3Service,
StateService stateService,
JobMetrics jobMetrics
)
Expand All @@ -69,6 +70,7 @@ JobMetrics jobMetrics
jobRepository,
jsonSerializerOptions,
memoryCacheService,
s3Service,
stateService,
redisConnectionString
);
Expand Down
1 change: 1 addition & 0 deletions apps/backend/Wowthing.Backend.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="AWSSDK.S3" Version="4.0.24.1" />
<PackageReference Include="CsvHelper" Version="33.1.0" />
<PackageReference Include="Imageflow.AllPlatforms" Version="0.15.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Analyzers" Version="10.0.7" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,50 +1,56 @@
<script lang="ts">
import { backgroundMap } from '@/data/backgrounds';
import { InventorySlot } from '@/enums/inventory-slot';
import { settingsState } from '@/shared/state/settings.svelte';
import { sharedState } from '@/shared/state/shared.svelte';
import { userStore } from '@/stores';
import type { BackgroundImage, Character } from '@/types';
import type { BackgroundImage } from '@/types';
import type { CharacterProps } from '@/types/props';

import Configure from './CharactersPaperdollConfigure.svelte';
import Equipped from './CharactersPaperdollEquipped.svelte';
import Stats from './CharactersPaperdollStats.svelte';
import { backgroundMap } from '@/data/backgrounds';

export let character: Character;
let { character }: CharacterProps = $props();

let selected = character.configuration.backgroundId;
let selected = $state(character.configuration.backgroundId);

let backgroundImage: BackgroundImage;
let characterImage: string;
let filter: string;
$: {
backgroundImage =
let { backgroundImage, filter } = $derived.by(() => {
const retBackgroundImage =
backgroundMap[
selected === -1 ? settingsState.value.characters.defaultBackgroundId : selected
];
characterImage = $userStore.images[`${character.id}-2`];

if (backgroundImage) {
let retFilter: string = null;
if (retBackgroundImage) {
const filterParts: string[] = [];

const brightness =
character.configuration.backgroundBrightness !== -1
? character.configuration.backgroundBrightness
: backgroundImage.defaultBrightness;
: retBackgroundImage.defaultBrightness;
const saturation =
character.configuration.backgroundSaturation !== -1
? character.configuration.backgroundSaturation
: backgroundImage.defaultSaturate;
: retBackgroundImage.defaultSaturate;

if (brightness != 10) {
filterParts.push(`brightness(${brightness / 10})`);
}
if (saturation != 10) {
filterParts.push(`saturate(${saturation / 10})`);
}
filter = filterParts.join(' ');
retFilter = filterParts.join(' ');
}
}

return { backgroundImage: retBackgroundImage, filter: retFilter };
});

let characterImage = $derived.by(() => {
const imagePath = $userStore.images[`${character.id}-2`];
const imageUrl = document.getElementById('app').getAttribute('data-image-url');
return imageUrl ? `${imageUrl}${imagePath}` : imagePath;
});

const leftSide: InventorySlot[] = [
InventorySlot.Head,
Expand Down
Loading
Loading