Skip to content

Commit

Permalink
Adding ability to set the ID of a collection on PUT
Browse files Browse the repository at this point in the history
  • Loading branch information
JackLewis-digirati committed Sep 23, 2024
1 parent 06401bb commit cd9db66
Show file tree
Hide file tree
Showing 8 changed files with 201 additions and 118 deletions.
14 changes: 8 additions & 6 deletions src/IIIFPresentation/API/Attributes/EtagCachingAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

namespace API.Attributes;

public class EtagCachingAttribute : ActionFilterAttribute
public class EtagCachingAttribute() : ActionFilterAttribute
{
// When a "304 Not Modified" response is to be sent back to the client, all headers apart from the following list should be stripped from the response to keep the response size minimal. See https://datatracker.ietf.org/doc/html/rfc7232#section-4.1:~:text=200%20(OK)%20response.-,The%20server%20generating%20a%20304,-response%20MUST%20generate
private static readonly string[] headersToKeepFor304 =
Expand All @@ -22,7 +22,7 @@ public class EtagCachingAttribute : ActionFilterAttribute
HeaderNames.Vary
};

private static readonly Dictionary<string, string> etagHashes = new();
// private static readonly Dictionary<string, string> etagHashes = new();

// Adds cache headers to response
public override async Task OnResultExecutionAsync(
Expand All @@ -32,6 +32,7 @@ ResultExecutionDelegate next
{
var request = context.HttpContext.Request;
var response = context.HttpContext.Response;
var eTagManager = context.HttpContext.RequestServices.GetService<IETagManager>();

// For more info on this technique, see https://stackoverflow.com/a/65901913 and https://www.madskristensen.net/blog/send-etag-headers-in-aspnet-core/ and https://gist.github.com/madskristensen/36357b1df9ddbfd123162cd4201124c4
var originalStream = response.Body;
Expand All @@ -55,7 +56,7 @@ ResultExecutionDelegate next
{
responseHeaders.ETag ??=
GenerateETag(memoryStream,
request.Path); // This request generates a hash from the response - this would come from S3 in live
request.Path, eTagManager!); // This request generates a hash from the response - this would come from S3 in live
}

var requestHeaders = request.GetTypedHeaders();
Expand Down Expand Up @@ -90,7 +91,7 @@ private static bool IsEtagSupported(HttpResponse response)
return true;
}

private static EntityTagHeaderValue GenerateETag(Stream stream, string path)
private static EntityTagHeaderValue GenerateETag(Stream stream, string path, IETagManager eTagManager)
{
var hashBytes = MD5.HashData(stream);
stream.Position = 0;
Expand All @@ -100,7 +101,7 @@ private static EntityTagHeaderValue GenerateETag(Stream stream, string path)
new EntityTagHeaderValue('"' + hashString +
'"');

etagHashes[path] = enityTagHeader.Tag.ToString();
eTagManager.UpsertETag(path, enityTagHeader.Tag.ToString());
return enityTagHeader;
}

Expand All @@ -126,6 +127,7 @@ public override async Task OnActionExecutionAsync(ActionExecutingContext context
ArgumentNullException.ThrowIfNull(next);

var request = context.HttpContext.Request;
var eTagManager = context.HttpContext.RequestServices.GetService<IETagManager>();

if (request.Method == HttpMethod.Put.ToString())
{
Expand All @@ -135,7 +137,7 @@ public override async Task OnActionExecutionAsync(ActionExecutingContext context
StatusCode = StatusCodes.Status400BadRequest
};

etagHashes.TryGetValue(request.Path, out var etag);
eTagManager!.TryGetETag(request.Path, out var etag);

if (!request.Headers.IfMatch.Equals(etag))
{
Expand Down
105 changes: 0 additions & 105 deletions src/IIIFPresentation/API/Features/Storage/Requests/UpdateCollection.cs

This file was deleted.

146 changes: 146 additions & 0 deletions src/IIIFPresentation/API/Features/Storage/Requests/UpsertCollection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
using API.Auth;
using API.Converters;
using API.Features.Storage.Helpers;
using API.Infrastructure.Helpers;
using API.Infrastructure.Requests;
using API.Settings;
using Core;
using Core.Helpers;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Models.API.Collection;
using Models.API.Collection.Upsert;
using Models.Database.Collections;
using Repository;
using Repository.Helpers;

namespace API.Features.Storage.Requests;

public class UpsertCollection(int customerId, string collectionId, UpsertFlatCollection collection, UrlRoots urlRoots, string? eTag)
: IRequest<ModifyEntityResult<FlatCollection>>
{
public int CustomerId { get; } = customerId;

public string CollectionId { get; set; } = collectionId;

public UpsertFlatCollection Collection { get; } = collection;

public UrlRoots UrlRoots { get; } = urlRoots;

public string? ETag { get; set; } = eTag;
}

public class UpsertCollectionHandler(
PresentationContext dbContext,
IETagManager eTagManager,
ILogger<CreateCollection> logger,
IOptions<ApiSettings> options)
: IRequestHandler<UpsertCollection, ModifyEntityResult<FlatCollection>>
{
private readonly ApiSettings settings = options.Value;

private const int DefaultCurrentPage = 1;

public async Task<ModifyEntityResult<FlatCollection>> Handle(UpsertCollection request, CancellationToken cancellationToken)
{
var databaseCollection =
await dbContext.Collections.FirstOrDefaultAsync(c => c.Id == request.CollectionId, cancellationToken);

if (databaseCollection == null)
{
var createdDate = DateTime.UtcNow;

var parentCollection = await dbContext.RetrieveCollection(request.CustomerId,
request.Collection.Parent.GetLastPathElement(), cancellationToken);
if (parentCollection == null)
{
return ModifyEntityResult<FlatCollection>.Failure(
"The parent collection could not be found", WriteResult.BadRequest);
}

databaseCollection = new Collection
{
Id = request.CollectionId,
Created = createdDate,
Modified = createdDate,
CreatedBy = Authorizer.GetUser(),
CustomerId = request.CustomerId,
IsPublic = request.Collection.Behavior.IsPublic(),
IsStorageCollection = request.Collection.Behavior.IsStorageCollection(),
Label = request.Collection.Label,
Parent = parentCollection.Id,
Slug = request.Collection.Slug,
Thumbnail = request.Collection.Thumbnail,
Tags = request.Collection.Tags,
ItemsOrder = request.Collection.ItemsOrder
};

await dbContext.AddAsync(databaseCollection, cancellationToken);
}
else
{
eTagManager.TryGetETag($"/{request.CustomerId}/collections/{request.CollectionId}", out var eTag);

if (request.ETag != eTag)
{
return ModifyEntityResult<FlatCollection>.Failure(
"ETag does not match", WriteResult.PreConditionFailed);
}

if (databaseCollection.Parent != request.Collection.Parent)
{
var parentCollection = await dbContext.RetrieveCollection(request.CustomerId,
request.Collection.Parent.GetLastPathElement(), cancellationToken);

if (parentCollection == null)
{
return ModifyEntityResult<FlatCollection>.Failure(
$"The parent collection could not be found", WriteResult.BadRequest);
}
}

databaseCollection.Modified = DateTime.UtcNow;
databaseCollection.ModifiedBy = Authorizer.GetUser();
databaseCollection.IsPublic = request.Collection.Behavior.IsPublic();
databaseCollection.IsStorageCollection = request.Collection.Behavior.IsStorageCollection();
databaseCollection.Label = request.Collection.Label;
databaseCollection.Parent = request.Collection.Parent;
databaseCollection.Slug = request.Collection.Slug;
databaseCollection.Thumbnail = request.Collection.Thumbnail;
databaseCollection.Tags = request.Collection.Tags;
databaseCollection.ItemsOrder = request.Collection.ItemsOrder;
}


var saveErrors = await dbContext.TrySaveCollection(request.CustomerId, logger, cancellationToken);

if (saveErrors != null)
{
return saveErrors;
}

var total = await dbContext.Collections.CountAsync(
c => c.CustomerId == request.CustomerId && c.Parent == databaseCollection.Id,
cancellationToken: cancellationToken);

var items = dbContext.Collections
.Where(s => s.CustomerId == request.CustomerId && s.Parent == databaseCollection.Id)
.Take(settings.PageSize);

foreach (var item in items)
{
item.FullPath = $"{(databaseCollection.Parent != null ? $"{databaseCollection.Slug}/" : string.Empty)}{item.Slug}";
}

if (databaseCollection.Parent != null)
{
databaseCollection.FullPath =
CollectionRetrieval.RetrieveFullPathForCollection(databaseCollection, dbContext);
}

return ModifyEntityResult<FlatCollection>.Success(
databaseCollection.ToFlatCollection(request.UrlRoots, settings.PageSize, DefaultCurrentPage, total,
await items.ToListAsync(cancellationToken: cancellationToken)));
}
}
10 changes: 6 additions & 4 deletions src/IIIFPresentation/API/Features/Storage/StorageController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ namespace API.Features.Storage;

[Route("/{customerId}")]
[ApiController]
public class StorageController(IOptions<ApiSettings> options, IMediator mediator)
public class StorageController(IOptions<ApiSettings> options, IMediator mediator, IETagManager etagManager)
: PresentationController(options.Value, mediator)
{
private readonly IETagManager etagManager = etagManager;

[HttpGet("{*slug}")]
[EtagCaching]
[EtagCaching()]
public async Task<IActionResult> GetHierarchicalCollection(int customerId, string slug = "")
{
var storageRoot = await Mediator.Send(new GetHierarchicalCollection(customerId, slug));
Expand Down Expand Up @@ -83,7 +85,6 @@ public async Task<IActionResult> Post(int customerId, [FromBody] UpsertFlatColle
}

[HttpPut("collections/{id}")]
[EtagCaching]
public async Task<IActionResult> Put(int customerId, string id, [FromBody] UpsertFlatCollection collection,
[FromServices] UpsertFlatCollectionValidator validator)
{
Expand All @@ -99,7 +100,8 @@ public async Task<IActionResult> Put(int customerId, string id, [FromBody] Upser
return this.ValidationFailed(validation);
}

return await HandleUpsert(new UpdateCollection(customerId, id, collection, GetUrlRoots()));
return await HandleUpsert(new UpsertCollection(customerId, id, collection, GetUrlRoots(),
Request.Headers.IfMatch));
}

[HttpDelete("collections/{id}")]
Expand Down
2 changes: 2 additions & 0 deletions src/IIIFPresentation/API/Infrastructure/ControllerBaseX.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ public static IActionResult ModifyResultToHttpResult<T>(this ControllerBase cont
$"{errorTitle}: Validation failed"),
WriteResult.StorageLimitExceeded => controller.PresentationProblem(entityResult.Error, instance, (int)HttpStatusCode.InsufficientStorage,
$"{errorTitle}: Storage limit exceeded"),
WriteResult.PreConditionFailed => controller.PresentationProblem(entityResult.Error, instance, (int)HttpStatusCode.PreconditionFailed,
$"{errorTitle}: Pre-condition failed"),
_ => controller.PresentationProblem(entityResult.Error, instance, (int)HttpStatusCode.InternalServerError, errorTitle),
};

Expand Down
Loading

0 comments on commit cd9db66

Please sign in to comment.