-
Notifications
You must be signed in to change notification settings - Fork 34
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Create Imageflow.Server.ExampleModernAPI and CustomMediaEndpoint
- Loading branch information
Showing
11 changed files
with
597 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
204 changes: 204 additions & 0 deletions
204
examples/Imageflow.Server.ExampleModernAPI/CustomMediaEndpoint.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,204 @@ | ||
using System.Buffers; | ||
using System.Text; | ||
using System.Text.Json; | ||
using Imazen.Abstractions.Blobs; | ||
using Imazen.Abstractions.Resulting; | ||
using Imazen.Routing.HttpAbstractions; | ||
using Imazen.Routing.Layers; | ||
using Imazen.Routing.Promises; | ||
using Imazen.Routing.Requests; | ||
|
||
namespace Imageflow.Server.ExampleModernAPI; | ||
|
||
|
||
internal record CustomFileData(string Path1, string QueryString1, string Path2, string QueryString2); | ||
|
||
/// <summary> | ||
/// This layer will capture requests for .json.custom paths. No .custom file actually exists, but the .json does, and we'll use that to determine the dependencies. | ||
/// </summary> | ||
public class CustomMediaLayer(PathMapper jsonFileMapper) : Imazen.Routing.Layers.IRoutingLayer | ||
{ | ||
public string Name => ".json.custom file handler"; | ||
|
||
public IFastCond? FastPreconditions => Conditions.HasPathSuffixOrdinalIgnoreCase(".json.custom"); | ||
public ValueTask<CodeResult<IRoutingEndpoint>?> ApplyRouting(MutableRequest request, CancellationToken cancellationToken = default) | ||
{ | ||
// FastPreconditions should have already been checked | ||
var result = jsonFileMapper.TryMapVirtualPath(request.Path.Replace(".json.custom", ".json")); | ||
if (result == null) | ||
{ | ||
// no mapping found | ||
return new ValueTask<CodeResult<IRoutingEndpoint>?>((CodeResult<IRoutingEndpoint>?)null); | ||
} | ||
var physicalPath = result.Value.MappedPhysicalPath; | ||
var lastWriteTimeUtc = File.GetLastWriteTimeUtc(physicalPath); | ||
if (lastWriteTimeUtc.Year == 1601) // file doesn't exist, pass to next middleware | ||
{ | ||
return new ValueTask<CodeResult<IRoutingEndpoint>?>((CodeResult<IRoutingEndpoint>?)null); | ||
} | ||
// Ok, the file exists. We can load and parse it using System.Text.Json to determine the dependencies.\ | ||
return RouteFromJsonFile(physicalPath, lastWriteTimeUtc, result.Value.MappingUsed, request, cancellationToken); | ||
} | ||
|
||
private async ValueTask<CodeResult<IRoutingEndpoint>?> RouteFromJsonFile(string jsonFilePath, DateTime lastWriteTimeUtc, IPathMapping mappingUsed, MutableRequest request, CancellationToken cancellationToken) | ||
{ | ||
// TODO: here, we could cache the json files in memory using a key based on jsonFilePath and lastWriteTimeUtc. | ||
|
||
var jsonText = await File.ReadAllTextAsync(jsonFilePath, cancellationToken); | ||
var data = JsonSerializer.Deserialize<CustomFileData>(jsonText); | ||
if (data == null) | ||
{ | ||
return CodeResult<IRoutingEndpoint>.Err((HttpStatus.ServerError, "Failed to parse .json custom data file")); | ||
} | ||
|
||
return new PromiseWrappingEndpoint(new CustomMediaPromise(request.ToSnapshot(true),data)); | ||
} | ||
} | ||
|
||
internal class CustomMediaPromise(IRequestSnapshot r, CustomFileData data) : ICacheableBlobPromise | ||
{ | ||
public bool IsCacheSupporting => true; | ||
public IRequestSnapshot FinalRequest { get; } = r; | ||
|
||
public async ValueTask<IAdaptableHttpResponse> CreateResponseAsync(IRequestSnapshot request, IBlobRequestRouter router, IBlobPromisePipeline pipeline, | ||
CancellationToken cancellationToken = default) | ||
{ | ||
// This code path isn't called, it's just to satisfy the primitive IInstantPromise interface. | ||
return new BlobResponse(await TryGetBlobAsync(request, router, pipeline, cancellationToken)); | ||
} | ||
|
||
public bool HasDependencies => true; | ||
public bool ReadyToWriteCacheKeyBasisData { get; private set; } | ||
|
||
/// <summary> | ||
/// Gets a promise for the given path that includes caching logic if indicated by the caching configuration and the latency by default. | ||
/// </summary> | ||
/// <param name="router"></param> | ||
/// <param name="childRequestUri"></param> | ||
/// <param name="cancellationToken"></param> | ||
/// <returns></returns> | ||
private async ValueTask<CodeResult<ICacheableBlobPromise>> RouteDependencyAsync(IBlobRequestRouter router, string childRequestUri, | ||
CancellationToken cancellationToken = default) | ||
{ | ||
if (FinalRequest.OriginatingRequest == null) | ||
{ | ||
return CodeResult<ICacheableBlobPromise>.ErrFrom(HttpStatus.BadRequest, "OriginatingRequest is required, but was null"); | ||
} | ||
var dependencyRequest = MutableRequest.ChildRequest(FinalRequest.OriginatingRequest, FinalRequest, childRequestUri, HttpMethods.Get); | ||
var routingResult = await router.RouteToPromiseAsync(dependencyRequest, cancellationToken); | ||
if (routingResult == null) | ||
{ | ||
return CodeResult<ICacheableBlobPromise>.ErrFrom(HttpStatus.NotFound, "Dependency not found: " + childRequestUri); | ||
} | ||
if (routingResult.TryUnwrapError(out var error)) | ||
{ | ||
return CodeResult<ICacheableBlobPromise>.Err(error.WithAppend("Error routing to dependency: " + childRequestUri)); | ||
} | ||
return CodeResult<ICacheableBlobPromise>.Ok(routingResult.Unwrap()); | ||
} | ||
public async ValueTask<CodeResult> RouteDependenciesAsync(IBlobRequestRouter router, CancellationToken cancellationToken = default) | ||
{ | ||
var uri1 = data.Path1 + data.QueryString1; | ||
var uri2 = data.Path2 + data.QueryString2; | ||
|
||
foreach (var uri in new[]{uri1, uri2}) | ||
{ | ||
var routingResult = await RouteDependencyAsync(router, uri, cancellationToken); | ||
if (routingResult.TryUnwrapError(out var error)) | ||
{ | ||
return CodeResult.Err(error); | ||
} | ||
Dependencies ??= new List<ICacheableBlobPromise>(); | ||
Dependencies.Add(routingResult.Unwrap()); | ||
} | ||
ReadyToWriteCacheKeyBasisData = true; | ||
return CodeResult.Ok(); | ||
} | ||
|
||
internal List<ICacheableBlobPromise>? Dependencies { get; private set; } | ||
|
||
private LatencyTrackingZone? latencyZone = null; | ||
/// <summary> | ||
/// Must route dependencies first! | ||
/// </summary> | ||
public LatencyTrackingZone? LatencyZone { | ||
get | ||
{ | ||
if (!ReadyToWriteCacheKeyBasisData) throw new InvalidOperationException("Dependencies must be routed first"); | ||
// produce a latency zone based on all dependency strings, joined, plus the sum of their latency defaults | ||
if (latencyZone != null) return latencyZone; | ||
var latency = 0; | ||
var sb = new StringBuilder(); | ||
sb.Append("customMediaSwitcher("); | ||
foreach (var dependency in Dependencies!) | ||
{ | ||
latency += dependency.LatencyZone?.DefaultMs ?? 0; | ||
sb.Append(dependency.LatencyZone?.TrackingZone ?? "(unknown)"); | ||
} | ||
sb.Append(")"); | ||
latencyZone = new LatencyTrackingZone(sb.ToString(), latency, true); //AlwaysShield is true (never skip caching) | ||
return latencyZone; | ||
} | ||
} | ||
|
||
public void WriteCacheKeyBasisPairsToRecursive(IBufferWriter<byte> writer) | ||
{ | ||
FinalRequest.WriteCacheKeyBasisPairsTo(writer); | ||
if (Dependencies == null) throw new InvalidOperationException("Dependencies must be routed first"); | ||
foreach (var dependency in Dependencies) | ||
{ | ||
dependency.WriteCacheKeyBasisPairsToRecursive(writer); | ||
} | ||
|
||
var otherCacheKeyData = 1; | ||
writer.WriteInt(otherCacheKeyData); | ||
} | ||
|
||
private byte[]? cacheKey32Bytes = null; | ||
public byte[] GetCacheKey32Bytes() | ||
{ | ||
return cacheKey32Bytes ??= this.GetCacheKey32BytesUncached(); | ||
} | ||
|
||
public bool SupportsPreSignedUrls => false; | ||
|
||
public async ValueTask<CodeResult<IBlobWrapper>> TryGetBlobAsync(IRequestSnapshot request, IBlobRequestRouter router, IBlobPromisePipeline pipeline, | ||
CancellationToken cancellationToken = default) | ||
{ | ||
// Our logic is to return whichever dependency is smaller. | ||
// This is a contrived example, but it's a good example of how to use dependencies. | ||
var blobWrappers = new List<IBlobWrapper>(); | ||
var smallestBlob = default(IBlobWrapper); | ||
try | ||
{ | ||
foreach (var dependency in Dependencies!) | ||
{ | ||
var result = await dependency.TryGetBlobAsync(request, router, pipeline, cancellationToken); | ||
if (result.TryUnwrapError(out var error)) | ||
{ | ||
return CodeResult<IBlobWrapper>.Err(error); | ||
} | ||
var blob = result.Unwrap(); | ||
blobWrappers.Add(blob); | ||
|
||
if (smallestBlob == null || blob.Attributes.EstimatedBlobByteCount < smallestBlob.Attributes.EstimatedBlobByteCount) | ||
{ | ||
smallestBlob = blob; | ||
} | ||
} | ||
if (smallestBlob == null) | ||
{ | ||
return CodeResult<IBlobWrapper>.ErrFrom(HttpStatus.NotFound, "No dependencies found"); | ||
} | ||
return CodeResult<IBlobWrapper>.Ok(smallestBlob.ForkReference()); | ||
} | ||
finally | ||
{ | ||
foreach (var blobWrapper in blobWrappers) | ||
{ | ||
blobWrapper.Dispose(); | ||
} | ||
} | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base | ||
USER $APP_UID | ||
WORKDIR /app | ||
EXPOSE 8080 | ||
EXPOSE 8081 | ||
|
||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build | ||
ARG BUILD_CONFIGURATION=Release | ||
WORKDIR /src | ||
COPY ["examples/Imageflow.Server.ExampleModernAPI/Imageflow.Server.ExampleModernAPI.csproj", "examples/Imageflow.Server.ExampleModernAPI/"] | ||
RUN dotnet restore "examples/Imageflow.Server.ExampleModernAPI/Imageflow.Server.ExampleModernAPI.csproj" | ||
COPY . . | ||
WORKDIR "/src/examples/Imageflow.Server.ExampleModernAPI" | ||
RUN dotnet build "Imageflow.Server.ExampleModernAPI.csproj" -c $BUILD_CONFIGURATION -o /app/build | ||
|
||
FROM build AS publish | ||
ARG BUILD_CONFIGURATION=Release | ||
RUN dotnet publish "Imageflow.Server.ExampleModernAPI.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false | ||
|
||
FROM base AS final | ||
WORKDIR /app | ||
COPY --from=publish /app/publish . | ||
ENTRYPOINT ["dotnet", "Imageflow.Server.ExampleModernAPI.dll"] |
32 changes: 32 additions & 0 deletions
32
examples/Imageflow.Server.ExampleModernAPI/Imageflow.Server.ExampleModernAPI.csproj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
<Project Sdk="Microsoft.NET.Sdk.Web"> | ||
|
||
<PropertyGroup> | ||
<TargetFramework>net8.0</TargetFramework> | ||
<Nullable>enable</Nullable> | ||
<ImplicitUsings>enable</ImplicitUsings> | ||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.3"/> | ||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0"/> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<Content Include="..\..\.dockerignore"> | ||
<Link>.dockerignore</Link> | ||
</Content> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<ProjectReference Include="..\..\src\Imageflow.Server\Imageflow.Server.csproj" /> | ||
<ProjectReference Include="..\..\src\Imazen.Abstractions\Imazen.Abstractions.csproj" /> | ||
<ProjectReference Include="..\..\src\Imazen.Common\Imazen.Common.csproj" /> | ||
<ProjectReference Include="..\..\src\Imazen.Routing\Imazen.Routing.csproj" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<Folder Include="wwwroot\images\" /> | ||
</ItemGroup> | ||
|
||
</Project> |
6 changes: 6 additions & 0 deletions
6
examples/Imageflow.Server.ExampleModernAPI/Imageflow.Server.ExampleModernAPI.http
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
@Imageflow.Server.ExampleModernAPI_HostAddress = http://localhost:5025 | ||
|
||
GET {{Imageflow.Server.ExampleModernAPI_HostAddress}}/img/test.json.custom | ||
|
||
|
||
### |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
using Imageflow.Fluent; | ||
using Imageflow.Server; | ||
using Imageflow.Server.ExampleModernAPI; | ||
using Imazen.Abstractions.Logging; | ||
using Imazen.Routing.Layers; | ||
using PathMapping = Imazen.Routing.Layers.PathMapping; | ||
|
||
var builder = WebApplication.CreateBuilder(args); | ||
|
||
// Add services to the container. | ||
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle | ||
builder.Services.AddEndpointsApiExplorer(); | ||
builder.Services.AddSwaggerGen(); | ||
|
||
builder.Services.AddImageflowLoggingSupport(); | ||
|
||
var app = builder.Build(); | ||
|
||
// Configure the HTTP request pipeline. | ||
if (app.Environment.IsDevelopment()) | ||
{ | ||
app.UseSwagger(); | ||
app.UseSwaggerUI(); | ||
} | ||
|
||
app.UseHttpsRedirection(); | ||
|
||
|
||
app.UseImageflow(new ImageflowMiddlewareOptions() | ||
.MapPath("/images", Path.Join(builder.Environment.WebRootPath, "images")) | ||
.SetMyOpenSourceProjectUrl("https://github.com/imazen/imageflow-dotnet-server") | ||
.AddRoutingConfiguration((routing) => | ||
{ | ||
routing.ConfigureEndpoints((endpoints) => | ||
{ | ||
endpoints.AddLayer(new CustomMediaLayer(new PathMapper(new[] | ||
{ | ||
new PathMapping("/img/", Path.Join(builder.Environment.ContentRootPath, "json"), true) | ||
}))); | ||
}); | ||
})); | ||
|
||
|
||
var summaries = new[] | ||
{ | ||
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" | ||
}; | ||
|
||
app.MapGet("/weatherforecast", () => | ||
{ | ||
var forecast = Enumerable.Range(1, 5).Select(index => | ||
new WeatherForecast | ||
( | ||
DateOnly.FromDateTime(DateTime.Now.AddDays(index)), | ||
Random.Shared.Next(-20, 55), | ||
summaries[Random.Shared.Next(summaries.Length)] | ||
)) | ||
.ToArray(); | ||
return forecast; | ||
}) | ||
.WithName("GetWeatherForecast") | ||
.WithOpenApi(); | ||
|
||
app.Run(); | ||
|
||
record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) | ||
{ | ||
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); | ||
} | ||
|
8 changes: 8 additions & 0 deletions
8
examples/Imageflow.Server.ExampleModernAPI/appsettings.Development.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
{ | ||
"Logging": { | ||
"LogLevel": { | ||
"Default": "Information", | ||
"Microsoft.AspNetCore": "Warning" | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"Logging": { | ||
"LogLevel": { | ||
"Default": "Information", | ||
"Microsoft.AspNetCore": "Warning" | ||
} | ||
}, | ||
"AllowedHosts": "*" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
{ | ||
"Path1": "/images/fire.jpg", | ||
"Path2": "/images/fire.jpg", | ||
"Querystring1": "format=webp&quality=80", | ||
"Querystring2": "format=jpeg&quality=76" | ||
} |
Oops, something went wrong.