2025-07-27 20:19:12 +07:00
|
|
|
using System.Buffers;
|
2025-07-27 16:02:56 +07:00
|
|
|
using System.IO.Pipelines;
|
|
|
|
|
using Microsoft.AspNetCore.Http.Json;
|
2025-07-27 23:22:07 +07:00
|
|
|
using Microsoft.Extensions.Caching.Memory;
|
2025-07-27 16:02:56 +07:00
|
|
|
using NetVips;
|
|
|
|
|
using Oh.My.Stitcher;
|
|
|
|
|
using Validation;
|
|
|
|
|
using ZLogger;
|
|
|
|
|
|
|
|
|
|
WebApplicationBuilder builder = WebApplication.CreateSlimBuilder(args);
|
|
|
|
|
builder.Logging.ClearProviders().AddZLoggerConsole();
|
2025-07-27 23:22:07 +07:00
|
|
|
builder.Services.AddMemoryCache();
|
2025-07-27 16:02:56 +07:00
|
|
|
builder.Services.Configure<JsonOptions>(options =>
|
|
|
|
|
{
|
|
|
|
|
options.SerializerOptions.TypeInfoResolver = StitchSerializerContext.Default;
|
|
|
|
|
});
|
|
|
|
|
WebApplication app = builder.Build();
|
|
|
|
|
|
|
|
|
|
ILoggerFactory loggerFactory = app.Services.GetRequiredService<ILoggerFactory>();
|
|
|
|
|
ILogger logger = loggerFactory.CreateLogger<Program>();
|
|
|
|
|
|
|
|
|
|
string? tilesDirectory = Environment.GetEnvironmentVariable("ASSET_PATH_RO");
|
2025-07-27 23:22:07 +07:00
|
|
|
string cacheDirectory = Path.Combine(Path.GetTempPath(), "oh-my-stitch");
|
|
|
|
|
Directory.CreateDirectory(cacheDirectory);
|
2025-07-27 16:02:56 +07:00
|
|
|
|
|
|
|
|
// sanity check
|
|
|
|
|
Assumes.NotNullOrEmpty(tilesDirectory);
|
|
|
|
|
Assumes.True(File.Exists(Path.Combine(tilesDirectory, "A1.png")));
|
|
|
|
|
Assumes.True(File.Exists(Path.Combine(tilesDirectory, "AE55.png")));
|
|
|
|
|
|
2025-07-27 23:22:07 +07:00
|
|
|
|
2025-07-27 16:02:56 +07:00
|
|
|
app.UseDefaultFiles();
|
|
|
|
|
app.UseStaticFiles();
|
2025-07-27 23:22:07 +07:00
|
|
|
app.MapPost("/api/image/generate", (Stitch request, IMemoryCache cache) =>
|
2025-07-27 16:02:56 +07:00
|
|
|
{
|
|
|
|
|
Pipe pipe = new();
|
|
|
|
|
_ = Task.Run(async () =>
|
|
|
|
|
{
|
2025-07-27 23:22:07 +07:00
|
|
|
Image? image = null;
|
2025-07-27 16:02:56 +07:00
|
|
|
List<Image> images = [];
|
|
|
|
|
try
|
|
|
|
|
{
|
2025-07-27 23:22:07 +07:00
|
|
|
bool created = Tile.TryCreate(in request, tilesDirectory, images, logger, cache, out image,
|
|
|
|
|
out string? cacheKey, out string? cacheFile);
|
|
|
|
|
if( !created && cacheFile != null )
|
|
|
|
|
{
|
|
|
|
|
logger.ZLogDebug($"cache hit key: {cacheKey}, file: {cacheFile}");
|
|
|
|
|
await using FileStream cacheStream = new(cacheFile, FileMode.Open, FileAccess.Read, FileShare.Read);
|
|
|
|
|
await cacheStream.CopyToAsync(pipe.Writer.AsStream());
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if( cacheKey == null )
|
|
|
|
|
{
|
|
|
|
|
image?.WriteToStream(pipe.Writer.AsStream(), ".png");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-27 20:19:12 +07:00
|
|
|
Pipe innerPipe = new();
|
|
|
|
|
_ = Task.Run(async () =>
|
|
|
|
|
{
|
2025-07-27 23:22:07 +07:00
|
|
|
string newCacheFile = Path.Combine(cacheDirectory, $"{cacheKey}.png");
|
|
|
|
|
MemoryCacheEntryOptions cacheEntryOptions = new MemoryCacheEntryOptions()
|
|
|
|
|
.SetSlidingExpiration(TimeSpan.FromMinutes(10))
|
|
|
|
|
.RegisterPostEvictionCallback((_, value, _, _) =>
|
|
|
|
|
{
|
|
|
|
|
if( value is string path )
|
|
|
|
|
File.Delete(path);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await using FileStream cacheStream = new(newCacheFile, FileMode.Create, FileAccess.Write, FileShare.Read);
|
2025-07-27 20:19:12 +07:00
|
|
|
while( true )
|
|
|
|
|
{
|
|
|
|
|
ReadResult result = await innerPipe.Reader.ReadAsync();
|
|
|
|
|
ReadOnlySequence<byte> buffer = result.Buffer;
|
2025-07-27 23:22:07 +07:00
|
|
|
|
|
|
|
|
foreach( ReadOnlyMemory<byte> segment in buffer )
|
|
|
|
|
await Task.WhenAll(pipe.Writer.WriteAsync(segment).AsTask(), cacheStream.WriteAsync(segment).AsTask());
|
|
|
|
|
|
|
|
|
|
innerPipe.Reader.AdvanceTo(buffer.End);
|
|
|
|
|
if( result.IsCompleted )
|
|
|
|
|
break;
|
2025-07-27 20:19:12 +07:00
|
|
|
}
|
2025-07-30 23:57:51 +07:00
|
|
|
logger.ZLogDebug($"save cache key: {cacheKey}, file: {newCacheFile}");
|
|
|
|
|
cache.Set(cacheKey, newCacheFile, cacheEntryOptions);
|
2025-07-27 20:19:12 +07:00
|
|
|
});
|
2025-07-27 23:22:07 +07:00
|
|
|
image?.WriteToStream(innerPipe.Writer.AsStream(), ".png");
|
2025-07-30 23:57:51 +07:00
|
|
|
await innerPipe.Writer.CompleteAsync();
|
2025-07-27 16:02:56 +07:00
|
|
|
}
|
|
|
|
|
catch( Exception e )
|
|
|
|
|
{
|
|
|
|
|
logger.ZLogError(e, $"Error when generating image");
|
|
|
|
|
using Image errorImage = Tile.CreateError(e);
|
|
|
|
|
errorImage.WriteToStream(pipe.Writer.AsStream(), ".png");
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
2025-07-27 23:22:07 +07:00
|
|
|
image?.Dispose();
|
2025-07-27 16:02:56 +07:00
|
|
|
foreach( Image img in images )
|
|
|
|
|
img.Dispose();
|
|
|
|
|
await pipe.Writer.CompleteAsync();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
return Results.Stream(pipe.Reader.AsStream(), "image/png");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
app.Run();
|