using System.Buffers; using System.IO.Pipelines; using Microsoft.AspNetCore.Http.Json; using Microsoft.Extensions.Caching.Memory; using NetVips; using Oh.My.Stitcher; using Validation; using ZLogger; WebApplicationBuilder builder = WebApplication.CreateSlimBuilder(args); builder.Logging.ClearProviders().AddZLoggerConsole(); builder.Services.AddMemoryCache(); builder.Services.Configure(options => { options.SerializerOptions.TypeInfoResolver = StitchSerializerContext.Default; }); WebApplication app = builder.Build(); ILoggerFactory loggerFactory = app.Services.GetRequiredService(); ILogger logger = loggerFactory.CreateLogger(); string? tilesDirectory = Environment.GetEnvironmentVariable("ASSET_PATH_RO"); string cacheDirectory = Path.Combine(Path.GetTempPath(), "oh-my-stitch"); Directory.CreateDirectory(cacheDirectory); // sanity check Assumes.NotNullOrEmpty(tilesDirectory); Assumes.True(File.Exists(Path.Combine(tilesDirectory, "A1.png"))); Assumes.True(File.Exists(Path.Combine(tilesDirectory, "AE55.png"))); app.UseDefaultFiles(); app.UseStaticFiles(); app.MapPost("/api/image/generate", (Stitch request, IMemoryCache cache) => { Pipe pipe = new(); _ = Task.Run(async () => { Image? image = null; List images = []; try { 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; } Pipe innerPipe = new(); _ = Task.Run(async () => { 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); }); logger.ZLogDebug($"save cache key: {cacheKey}, file: {newCacheFile}"); cache.Set(cacheKey!, newCacheFile, cacheEntryOptions); await using FileStream cacheStream = new(newCacheFile, FileMode.Create, FileAccess.Write, FileShare.Read); while( true ) { ReadResult result = await innerPipe.Reader.ReadAsync(); ReadOnlySequence buffer = result.Buffer; foreach( ReadOnlyMemory segment in buffer ) await Task.WhenAll(pipe.Writer.WriteAsync(segment).AsTask(), cacheStream.WriteAsync(segment).AsTask()); innerPipe.Reader.AdvanceTo(buffer.End); if( result.IsCompleted ) break; } }); image?.WriteToStream(innerPipe.Writer.AsStream(), ".png"); } catch( Exception e ) { logger.ZLogError(e, $"Error when generating image"); using Image errorImage = Tile.CreateError(e); errorImage.WriteToStream(pipe.Writer.AsStream(), ".png"); } finally { image?.Dispose(); foreach( Image img in images ) img.Dispose(); await pipe.Writer.CompleteAsync(); } }); return Results.Stream(pipe.Reader.AsStream(), "image/png"); }); app.Run();