Skip to content

Commit

Permalink
initial commit adding ability to update the canvas_paintings table wi…
Browse files Browse the repository at this point in the history
…th asset details from the DLCS
  • Loading branch information
JackLewis-digirati committed Dec 19, 2024
1 parent cba835e commit 3bc4607
Show file tree
Hide file tree
Showing 21 changed files with 782 additions and 13 deletions.
16 changes: 13 additions & 3 deletions src/IIIFPresentation/API/Helpers/PathGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using API.Infrastructure.Requests;
using DLCS;
using DLCS.Models;
using Microsoft.Extensions.Options;
using Models.Database.Collections;
using Models.Database.General;
Expand Down Expand Up @@ -92,12 +93,21 @@ public string GenerateCanvasId(CanvasPainting canvasPainting)
public Uri? GenerateAssetUri(CanvasPainting canvasPainting)
{
if (string.IsNullOrEmpty(canvasPainting.AssetId)) return null;
var assetId = canvasPainting.AssetId.Split('/');
if (assetId.Length != 3) return null;

AssetId assetId;

try
{
assetId = AssetId.FromString(canvasPainting.AssetId);
}
catch // swallow error as it's not needed
{
return null;
}

var uriBuilder = new UriBuilder(dlcsSettings.ApiUri)
{
Path = $"/customers/{assetId[0]}/spaces/{assetId[1]}/images/{assetId[2]}",
Path = $"/customers/{assetId.Customer}/spaces/{assetId.Space}/images/{assetId.Asset}",
};
return uriBuilder.Uri;
}
Expand Down
4 changes: 3 additions & 1 deletion src/IIIFPresentation/AWS/Settings/AWSSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

public class AWSSettings
{
public const string SettingsName = "AWS";

/// <summary>
/// If true, service will use LocalStack and custom ServiceUrl
/// </summary>
Expand All @@ -16,4 +18,4 @@ public class AWSSettings
/// SQS Settings
/// </summary>
public SQSSettings SQS { get; set; } = new();
}
}
7 changes: 6 additions & 1 deletion src/IIIFPresentation/AWS/Settings/SQSSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ public class SQSSettings
/// </summary>
public string? CustomerCreatedQueueName { get; set; }

/// <summary>
/// Name of queue that will receive notifications when a batch is completed
/// </summary>
public string? BatchCompletionQueueName { get; set; }

/// <summary>
/// The duration (in seconds) for which the call waits for a message to arrive in the queue before returning
/// </summary>
Expand All @@ -21,4 +26,4 @@ public class SQSSettings
/// Service root for SQS. Ignored if not running LocalStack
/// </summary>
public string ServiceUrl { get; set; } = "http://localhost:4566/";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace BackgroundHandler.Tests.BatchCompletion;

public class BatchCompletionMessageHandlerTests
{

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

<ItemGroup>
<ProjectReference Include="..\AWS\AWS.csproj" />
<ProjectReference Include="..\DLCS\DLCS.csproj" />
<ProjectReference Include="..\Repository\Repository.csproj" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace BackgroundHandler.BatchCompletion;

public record BatchCompletionMessage(
int Id,
int CustomerId,
int Total,
int Success,
int Errors,
bool Superseded,
DateTime Started,
DateTime Finished);
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
using System.Text.Json;
using AWS.SQS;
using BackgroundHandler.Helpers;
using Core.Helpers;
using DLCS;
using DLCS.API;
using DLCS.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Models.Database.General;
using Repository;
using Batch = Models.Database.General.Batch;

namespace BackgroundHandler.BatchCompletion;

public class BatchCompletionMessageHandler(
PresentationContext dbContext,
IDlcsApiClient dlcsApiClient,
IOptions<DlcsSettings> dlcsOptions,
ILogger<BatchCompletionMessageHandler> logger)
: IMessageHandler
{
private static readonly JsonSerializerOptions JsonSerializerOptions = new(JsonSerializerDefaults.Web);

private readonly DlcsSettings dlcsSettings = dlcsOptions.Value;

public async Task<bool> HandleMessage(QueueMessage message, CancellationToken cancellationToken)
{
using (LogContextHelpers.SetServiceName(nameof(BatchCompletionMessageHandler)))
{
try
{
var batchCompletionMessage = DeserializeMessage(message);

await UpdateAssetsIfRequired(batchCompletionMessage, cancellationToken);
return true;
}
catch (Exception ex)
{
logger.LogError(ex, "Error handling batch-completion message {MessageId}", message.MessageId);
}
}

return false;
}

private async Task UpdateAssetsIfRequired(BatchCompletionMessage batchCompletionMessage, CancellationToken cancellationToken)
{
var batch = await dbContext.Batches.Include(b => b.Manifest)
.ThenInclude(m => m.CanvasPaintings)
.FirstOrDefaultAsync(b => b.Id == batchCompletionMessage.Id, cancellationToken);

// batch isn't tracked by presentation, so nothing to do
if (batch == null) return;

// Other batches haven't completed, so no point populating items until all are complete
if (await dbContext.Batches.AnyAsync(b => b.ManifestId == batch.ManifestId && b.Status != BatchStatus.Completed,
cancellationToken))
{
return;
}

logger.LogInformation(
"Attempting to complete assets in batch {BatchId} for customer {CustomerId} with the manifest {ManifestId}",
batch.Id, batch.CustomerId, batch.ManifestId);

var assets = await RetrieveImages(batch, cancellationToken);

UpdateCanvasPaintings(assets, batch);
CompleteBatch(batch);

await dbContext.SaveChangesAsync(cancellationToken);

logger.LogTrace("updating batch {BatchId} has been completed", batch.Id);
}

private void CompleteBatch(Batch batch)
{
batch.Finished = DateTime.UtcNow; //todo: change to actual batch completion time?
batch.Status = BatchStatus.Completed;
}

private void UpdateCanvasPaintings(HydraCollection<Asset> assets, Batch batch)
{
if (batch.Manifest?.CanvasPaintings == null) return;

foreach (var canvasPainting in batch.Manifest.CanvasPaintings)
{
if (canvasPainting.CanvasOriginalId == null) // Trying to figure out an asset that hasn't been updated
{
var assetId = AssetId.FromString(canvasPainting.AssetId!);

var asset = assets.Members.FirstOrDefault(a => a.ResourceId!.Contains($"{assetId.Space}/images/{assetId.Asset}"));
if (asset == null || asset.Ingesting) continue;

canvasPainting.CanvasOriginalId =
new Uri(
$"{dlcsSettings.OrchestratorUri}/iiif-img/{assetId.Customer}/{assetId.Space}/{assetId.Asset}/full/max/0/default.jpg"); //todo: do we need this? Supposed to be null for an asset really
canvasPainting.Thumbnail =
new Uri(
$"{dlcsSettings.OrchestratorUri}/thumbs/{assetId.Customer}/{assetId.Space}/{assetId.Asset}/100,/max/0/default.jpg"); //todo: move this to class
canvasPainting.Modified = DateTime.UtcNow;
canvasPainting.StaticHeight = asset.Height;
canvasPainting.StaticWidth = asset.Width;
}
}
}

private async Task<HydraCollection<Asset>> RetrieveImages(Batch batch, CancellationToken cancellationToken)
{
var assetsRequest =
batch.Manifest?.CanvasPaintings?.Where(c => c.AssetId != null).Select(c => c.AssetId!).ToList() ?? [];

return await dlcsApiClient.RetrieveAllImages(batch.CustomerId, assetsRequest, cancellationToken);
}

private static BatchCompletionMessage DeserializeMessage(QueueMessage message)
{
var deserialized = JsonSerializer.Deserialize<BatchCompletionMessage>(message.Body, JsonSerializerOptions);
return deserialized.ThrowIfNull(nameof(deserialized));
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using AWS.Configuration;
using AWS.Settings;
using AWS.SQS;
using BackgroundHandler.BatchCompletion;
using BackgroundHandler.CustomerCreation;
using BackgroundHandler.Listener;
using Repository;
Expand Down Expand Up @@ -33,6 +34,14 @@ public static IServiceCollection AddBackgroundServices(this IServiceCollection s
ActivatorUtilities.CreateInstance<CreateBackgroundListenerService<CustomerCreatedMessageHandler>>(sp, aws.SQS.CustomerCreatedQueueName))
.AddScoped<CustomerCreatedMessageHandler>();
}

if (!string.IsNullOrEmpty(aws.SQS.BatchCompletionQueueName))
{
services
.AddHostedService(sp =>
ActivatorUtilities.CreateInstance<CreateBackgroundListenerService<BatchCompletionMessageHandler>>(sp, aws.SQS.BatchCompletionQueueName))
.AddScoped<BatchCompletionMessageHandler>();
}

return services;
}
Expand All @@ -45,4 +54,4 @@ public static IServiceCollection AddDataAccess(this IServiceCollection services,
return services
.AddPresentationContext(configuration);
}
}
}
16 changes: 11 additions & 5 deletions src/IIIFPresentation/BackgroundHandler/Program.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using AWS.Settings;
using BackgroundHandler.Infrastructure;
using BackgroundHandler.Settings;
using DLCS;
using Serilog;

var builder = WebApplication.CreateBuilder(args);
Expand All @@ -15,12 +16,17 @@

builder.Services.AddOptions<BackgroundHandlerSettings>()
.BindConfiguration(string.Empty);
var aws = builder.Configuration.GetSection("AWS").Get<AWSSettings>() ?? new AWSSettings();
var aws = builder.Configuration.GetSection(AWSSettings.SettingsName).Get<AWSSettings>() ?? new AWSSettings();
var dlcsSettings = builder.Configuration.GetSection(DlcsSettings.SettingsName);
var dlcs = dlcsSettings.Get<DlcsSettings>()!;

builder.Services.AddAws(builder.Configuration, builder.Environment);
builder.Services.AddDataAccess(builder.Configuration);
builder.Services.AddBackgroundServices(aws);
builder.Services.AddAws(builder.Configuration, builder.Environment)
.AddDataAccess(builder.Configuration)
.AddHttpContextAccessor()
.AddDlcsClientWithLocalAuth(dlcs)
.AddBackgroundServices(aws)
.Configure<DlcsSettings>(dlcsSettings);

var app = builder.Build();

app.Run();
app.Run();
21 changes: 21 additions & 0 deletions src/IIIFPresentation/DLCS/API/DlcsApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using DLCS.Models;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json.Linq;

namespace DLCS.API;

Expand All @@ -26,6 +27,12 @@ public interface IDlcsApiClient
/// </summary>
public Task<List<Batch>> IngestAssets<T>(int customerId, List<T> images,
CancellationToken cancellationToken = default);

/// <summary>
/// Retrieve a list of assets from the DLCS
/// </summary>
public Task<HydraCollection<Asset>> RetrieveAllImages(int customerId, List<string> assets,
CancellationToken cancellationToken = default);
}

/// <summary>
Expand Down Expand Up @@ -85,6 +92,20 @@ public async Task<List<Batch>> IngestAssets<T>(int customerId, List<T> assets, C
return batches.ToList();
}

public async Task<HydraCollection<Asset>> RetrieveAllImages(int customerId, List<string> assets,
CancellationToken cancellationToken = default)
{
logger.LogTrace("performing an all images call for customer {CustomerId}", customerId);
var allImagesPath = $"/customers/{customerId}/allImages";

var allImagesRequest = new AllImages(assets);

var asset =
await CallDlcsApi<HydraCollection<Asset>>(HttpMethod.Post, allImagesPath, allImagesRequest, cancellationToken);

return asset ?? throw new DlcsException("Failed to retrieve all images");
}

private async Task<T?> CallDlcsApi<T>(HttpMethod httpMethod, string path, object payload,
CancellationToken cancellationToken)
{
Expand Down
10 changes: 10 additions & 0 deletions src/IIIFPresentation/DLCS/DlcsSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ public class DlcsSettings
/// URL root of DLCS API
/// </summary>
public required Uri ApiUri { get; set; }

/// <summary>
/// URL root of DLCS API
/// </summary>
public Uri? OrchestratorUri { get; set; }

/// <summary>
/// Default timeout (in ms) use for HttpClient.Timeout.
Expand All @@ -18,4 +23,9 @@ public class DlcsSettings
/// The maximum size of an individual batch request
/// </summary>
public int MaxBatchSize { get; set; } = 100;

/// <summary>
/// Used to authenticate requests that do not go via the HttpContextAccessor
/// </summary>
public string ApiLocalAuth { get; set; } //Todo: is this right? is there a better way of setting auth
}
2 changes: 1 addition & 1 deletion src/IIIFPresentation/DLCS/Handlers/AmbientAuthHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@ protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage reques
request.Headers.Authorization = authHeader;
return base.SendAsync(request, cancellationToken);
}
}
}
21 changes: 21 additions & 0 deletions src/IIIFPresentation/DLCS/Handlers/AmbientAuthLocalHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System.Net.Http.Headers;
using Microsoft.Extensions.Options;

namespace DLCS.Handlers;

public class AmbientAuthLocalHandler() : DelegatingHandler

Check warning on line 6 in src/IIIFPresentation/DLCS/Handlers/AmbientAuthLocalHandler.cs

View workflow job for this annotation

GitHub Actions / test-dotnet

Non-nullable field 'dlcsSettings' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.
{
private readonly DlcsSettings dlcsSettings;

public AmbientAuthLocalHandler(IOptions<DlcsSettings> dlcsOptions) : this()
{
dlcsSettings = dlcsOptions.Value;
}

protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
request.Headers.Authorization = new AuthenticationHeaderValue("Basic", dlcsSettings.ApiLocalAuth);
return base.SendAsync(request, cancellationToken);
}
}
23 changes: 23 additions & 0 deletions src/IIIFPresentation/DLCS/Models/AllImages.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System.Text.Json.Serialization;

namespace DLCS.Models;

public class AllImages : JsonLdBase
{
public AllImages(List<string> members)
{
Members = members.Select(m => new AllImagesMember(m)).ToList();
Type = "Collection" ;
}

[JsonPropertyOrder(3)]
[JsonPropertyName("member")]
public List<AllImagesMember> Members { get; set; }
}

public class AllImagesMember(string id)
{
public string Id { get; set; } = id;
}


Loading

0 comments on commit 3bc4607

Please sign in to comment.