From 5d88ad49c0b1c5cdad87fdbde8e6c1da4a01249e Mon Sep 17 00:00:00 2001 From: Timerix Date: Sun, 23 Mar 2025 02:49:02 +0500 Subject: [PATCH] API methods reimplemented for DTLib.Web --- ParadoxSaveParser.Lib/BufferedEnumerator.cs | 6 +- ParadoxSaveParser.Lib/Parser.cs | 6 +- ParadoxSaveParser.WebAPI/Config.cs | 2 +- .../ParadoxSaveParser.WebAPI.csproj | 2 +- ParadoxSaveParser.WebAPI/Program.cs | 145 ++++++++---------- ParadoxSaveParser.WebAPI/SaveFileMetadata.cs | 3 +- TODO.txt | 6 +- 7 files changed, 79 insertions(+), 91 deletions(-) diff --git a/ParadoxSaveParser.Lib/BufferedEnumerator.cs b/ParadoxSaveParser.Lib/BufferedEnumerator.cs index 329d450..7d244aa 100644 --- a/ParadoxSaveParser.Lib/BufferedEnumerator.cs +++ b/ParadoxSaveParser.Lib/BufferedEnumerator.cs @@ -23,7 +23,7 @@ namespace ParadoxSaveParser.Lib; /// /// Console.Write($"| {cur.Value} |"); /// -/// for (var next = cur.Next; next != null; next = next.Next) +/// for (var next = cur.Next; next is not null; next = next.Next) /// Console.Write($" {next.Value}"); /// Console.WriteLine(); /// } @@ -64,8 +64,8 @@ public class BufferedEnumerator : IEnumerator> return false; _currentNodeIndex++; - _currentNode = _currentNode == null ? _llist.First : _currentNode.Next; - return _currentNode != null; + _currentNode = _currentNode is null ? _llist.First : _currentNode.Next; + return _currentNode is not null; } public void Reset() diff --git a/ParadoxSaveParser.Lib/Parser.cs b/ParadoxSaveParser.Lib/Parser.cs index 1395c19..0d1150b 100644 --- a/ParadoxSaveParser.Lib/Parser.cs +++ b/ParadoxSaveParser.Lib/Parser.cs @@ -1,7 +1,7 @@ global using System; +global using System.Collections.Generic; global using System.IO; global using System.Text; -global using System.Collections.Generic; namespace ParadoxSaveParser.Lib; @@ -217,7 +217,7 @@ public class Parser if(!_tokens.MoveNext()) throw new Exception("Unexpected end of file"); object? value = ParseValue(); - if (value == null) + if (value is null) break; list.Add(value); } @@ -277,7 +277,7 @@ public class Parser throw new UnexpectedTokenException(tok); object? value = ParseValue(); - if (value == null) + if (value is null) throw new UnexpectedTokenException(_tokens.Current.Value); if(!dict.TryGetValue(key, out List? list)) diff --git a/ParadoxSaveParser.WebAPI/Config.cs b/ParadoxSaveParser.WebAPI/Config.cs index 12fba0b..5946402 100644 --- a/ParadoxSaveParser.WebAPI/Config.cs +++ b/ParadoxSaveParser.WebAPI/Config.cs @@ -7,7 +7,7 @@ public class Config public const int ActualVersion = 1; public int Version = ActualVersion; - public string BaseUrl = "http://127.0.0.1:8080/"; + public string BaseUrl = "http://127.0.0.1:5226/"; public static Config FromDtsod(DtsodV23 d) { diff --git a/ParadoxSaveParser.WebAPI/ParadoxSaveParser.WebAPI.csproj b/ParadoxSaveParser.WebAPI/ParadoxSaveParser.WebAPI.csproj index 7a8b9f4..c12050e 100644 --- a/ParadoxSaveParser.WebAPI/ParadoxSaveParser.WebAPI.csproj +++ b/ParadoxSaveParser.WebAPI/ParadoxSaveParser.WebAPI.csproj @@ -13,7 +13,7 @@ - + diff --git a/ParadoxSaveParser.WebAPI/Program.cs b/ParadoxSaveParser.WebAPI/Program.cs index f540d3d..4353c7c 100644 --- a/ParadoxSaveParser.WebAPI/Program.cs +++ b/ParadoxSaveParser.WebAPI/Program.cs @@ -4,7 +4,6 @@ global using System.Text; global using System.Text.Json; global using System.Threading; global using System.Threading.Tasks; -global using DTLib; global using DTLib.Demystifier; global using DTLib.Filesystem; global using DTLib.Logging; @@ -30,8 +29,9 @@ public class Program private static readonly IOPath _configPath = "./config.dtsod"; private static Config _config = new(); private static readonly ILogger _loggerRoot = new ConsoleLogger(); + private static readonly CancellationTokenSource _mainCancel = new(); private static ConcurrentDictionary _saveMetadataStorage = new(); - + private static JsonSerializerOptions _saveSerializerOptions = new() { WriteIndented = false, @@ -39,16 +39,16 @@ public class Program MaxDepth = 1024, }; + public static void Main(string[] args) { Console.InputEncoding = Encoding.UTF8; Console.OutputEncoding = Encoding.UTF8; ContextLogger logger = new ContextLogger(nameof(Main), _loggerRoot); - CancellationTokenSource mainCancel = new(); Console.CancelKeyPress += (_, _) => { - logger.LogInfo("stopping server..."); - mainCancel.Cancel(); + logger.LogInfo("Ctrl+C Pressed"); + _mainCancel.Cancel(); }; try @@ -67,26 +67,11 @@ public class Program // http server var router = new SimpleRouter(_loggerRoot); router.DefaultRoute = new ServeFilesRouteHandler("public"); - router.MapRoute("/aaa", async ctx => - { - byte[] buffer = - """ - - - -

aaa

- - - """.ToBytes(); - ctx.Response.ContentLength64 = buffer.Length; - await ctx.Response.OutputStream.WriteAsync(buffer); - return HttpStatusCode.OK; - }); - // router.MapGet("/getSaveStatus", GetSaveStatusHandler); - // router.MapPost("/uploadSave/eu4", UploadSaveHandler); - // app.MapPost("/parseSave/eu4", ParseSaveEU4Handler); + router.MapRoute("/getSaveStatus", HttpMethod.GET, GetSaveStatusHandler); + router.MapRoute("/uploadSave/eu4", HttpMethod.POST, UploadSaveHandler); + router.MapRoute("/parseSave/eu4", HttpMethod.POST, ParseSaveEU4Handler); - var app = new WebApp(_config.BaseUrl, _loggerRoot, router, mainCancel.Token); + var app = new WebApp(_config.BaseUrl, _loggerRoot, router, _mainCancel.Token); app.Run().GetAwaiter().GetResult(); } catch (OperationCanceledException ex) @@ -108,7 +93,8 @@ public class Program SearchOption.TopDirectoryOnly)) { using var metaFile = File.OpenRead(metaFilePath); - var meta = JsonSerializer.Deserialize(metaFile) ?? throw new NullReferenceException(metaFilePath); + var meta = JsonSerializer.Deserialize(metaFile) ?? + throw new NullReferenceException(metaFilePath); if (meta.status != SaveFileProcessingStatus.Done) { _loggerRoot.LogWarn(nameof(PrepareLocalFiles), $"metadata file '{metaFilePath}' status has invalid status {meta.status}"); @@ -118,85 +104,88 @@ public class Program throw new Exception("Guid collision!"); } } - - /* - private static async Task UploadSaveHandler(HttpContext httpContext) + + public record ErrorMessage(string errorMessage); + + private static async TaskReturnResponse(HttpListenerContext ctx, HttpStatusCode statusCode, object response) { - var remoteFile = httpContext.Request.Form.Files.FirstOrDefault(); - if (remoteFile is null || !remoteFile.FileName.EndsWith(".eu4")) - { - throw new BadHttpRequestException($"Invalid file format: {remoteFile?.FileName}", - StatusCodes.Status400BadRequest); - } + await JsonSerializer.SerializeAsync(ctx.Response.OutputStream, response, response.GetType(), + JsonSerializerOptions.Default, _mainCancel.Token); + ctx.Response.StatusCode = (int)statusCode; + return statusCode; + } + + private static async TaskReturnResponse(HttpListenerContext ctx, HttpStatusCode statusCode, string response) + { + await ctx.Response.OutputStream.WriteAsync(response.ToBytes(), _mainCancel.Token); + ctx.Response.StatusCode = (int)statusCode; + return statusCode; + } + + private static async TaskUploadSaveHandler(HttpListenerContext ctx) + { + string? contentType = ctx.Request.Headers.GetValues("Content-Type")?.FirstOrDefault(); + if (contentType != "application/octet-stream") + return await ReturnResponse(ctx, HttpStatusCode.BadRequest, + new ErrorMessage($"Invalid request Content-Type: '{contentType}'")); string saveId = Guid.NewGuid().ToString(); IOPath metaFilePath = PathHelper.GetMetaFilePath(saveId); if (File.Exists(metaFilePath)) - { - httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError; - throw new BadHttpRequestException($"Guid collision! file {metaFilePath} already exists.", StatusCodes.Status500InternalServerError); - } + return await ReturnResponse(ctx, HttpStatusCode.InternalServerError, + new ErrorMessage($"Guid collision! file' {metaFilePath}' already exists.")); var meta = new SaveFileMetadata { id = saveId, game = Game.EU4, status = SaveFileProcessingStatus.Initialized, }; if (!_saveMetadataStorage.TryAdd(saveId, meta)) - { - throw new BadHttpRequestException($"Guid collision! Can't create metadata with id {saveId}", StatusCodes.Status500InternalServerError); - } + return await ReturnResponse(ctx, HttpStatusCode.InternalServerError, + new ErrorMessage($"Guid collision! Can't create metadata with id {saveId}")); meta.status = SaveFileProcessingStatus.Uploading; IOPath saveFilePath = PathHelper.GetSaveFilePath(meta.id); await using var saveFile = File.OpenWrite(saveFilePath); - await using var remoteStream = remoteFile.OpenReadStream(); - await remoteStream.CopyToAsync(saveFile); + await using var remoteStream = ctx.Request.InputStream; + await remoteStream.CopyToAsync(saveFile, _mainCancel.Token); meta.status = SaveFileProcessingStatus.Uploaded; - await httpContext.Response.WriteAsJsonAsync(meta); + return await ReturnResponse(ctx, HttpStatusCode.OK, saveId); } - private static SaveFileMetadata GetMetaFromRequestId(HttpContext httpContext, string requestParamName) + private static (SaveFileMetadata? meta, ErrorMessage? errorMesage) GetMetaFromRequestId(HttpListenerContext ctx, string requestParamName) { - httpContext.Request.Query.TryGetValue(requestParamName, out var ids); - string? id = ids.FirstOrDefault(); + var ids = ctx.Request.QueryString.GetValues(requestParamName); + string? id = ids?.FirstOrDefault(); if (string.IsNullOrEmpty(id)) - { - throw new BadHttpRequestException($"No request parameter '{requestParamName}' provided", - StatusCodes.Status400BadRequest); - } + return (null, new ErrorMessage($"No request parameter '{requestParamName}' provided")); if (!_saveMetadataStorage.TryGetValue(id, out var meta)) - { - throw new BadHttpRequestException($"Save with {id} not found", - StatusCodes.Status400BadRequest); - } + return (null,new ErrorMessage($"Save with {id} not found")); - return meta; + return (meta, null); } - private static async Task GetSaveStatusHandler(HttpContext httpContext) + private static async TaskGetSaveStatusHandler(HttpListenerContext ctx) { - var meta = GetMetaFromRequestId(httpContext, "id"); - await httpContext.Response.WriteAsJsonAsync(meta); + var (meta, errorMessage) = GetMetaFromRequestId(ctx, "id"); + if(errorMessage is not null) + return await ReturnResponse(ctx, HttpStatusCode.InternalServerError, errorMessage); + + return await ReturnResponse(ctx, HttpStatusCode.OK, meta!); } - private static async Task ParseSaveEU4Handler(HttpContext httpContext) + private static async TaskParseSaveEU4Handler(HttpListenerContext ctx) { - var meta = GetMetaFromRequestId(httpContext, "id"); - if (meta.status == SaveFileProcessingStatus.Error) - { - httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError; - await httpContext.Response.WriteAsJsonAsync(meta); - return; - } + var (meta, errorMessage) = GetMetaFromRequestId(ctx, "id"); + if(errorMessage is not null) + return await ReturnResponse(ctx, HttpStatusCode.InternalServerError, errorMessage); try { - if (meta.status != SaveFileProcessingStatus.Uploaded) - throw new Exception($"Invalid save processing status: {meta.status}"); - - using var zipArchive = ZipFile.Open(PathHelper.GetSaveFilePath(meta.id).Str, ZipArchiveMode.Read); + using var zipArchive = ZipFile.Open(PathHelper.GetSaveFilePath(meta!.id).Str, ZipArchiveMode.Read); var zipEntry = zipArchive.Entries.FirstOrDefault(e => e.Name == "gamestate"); if (zipEntry is null) - throw new Exception("Invalid save format: no gamestate file found"); + return await ReturnResponse(ctx, HttpStatusCode.BadRequest, + new ErrorMessage("Invalid save format: no 'gamestate' file found")); + string extractedGamestatePath = PathHelper.GetSaveFilePath(meta.id) + ".gamestate"; zipEntry.ExtractToFile(extractedGamestatePath); var gamestateStream = File.OpenRead(extractedGamestatePath); @@ -208,20 +197,18 @@ public class Program meta.status = SaveFileProcessingStatus.SavingResults; IOPath resultFilePath = PathHelper.GetParsedSaveFilePath(meta.id); await using var resultFile = File.OpenWrite(resultFilePath); - await JsonSerializer.SerializeAsync(resultFile, result, _saveSerializerOptions); + await JsonSerializer.SerializeAsync(resultFile, result, _saveSerializerOptions, _mainCancel.Token); meta.status = SaveFileProcessingStatus.Done; meta.SaveToFile(); } catch (Exception ex) { - meta.status = SaveFileProcessingStatus.Error; string errorMesage = ex.ToStringDemystified(); - meta.errorMesage = errorMesage; - httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError; - _app.Logger.Log(LogLevel.Error, "ParseSaveEU4 Error: {errorMesage}", errorMesage); + _loggerRoot.LogWarn(nameof(ParseSaveEU4Handler), errorMesage); + return await ReturnResponse(ctx, HttpStatusCode.BadRequest, + new ErrorMessage(errorMesage)); } - await httpContext.Response.WriteAsJsonAsync(meta); + return await ReturnResponse(ctx, HttpStatusCode.OK, meta); } - */ } \ No newline at end of file diff --git a/ParadoxSaveParser.WebAPI/SaveFileMetadata.cs b/ParadoxSaveParser.WebAPI/SaveFileMetadata.cs index 1526e0b..c8e1dac 100644 --- a/ParadoxSaveParser.WebAPI/SaveFileMetadata.cs +++ b/ParadoxSaveParser.WebAPI/SaveFileMetadata.cs @@ -4,7 +4,7 @@ namespace ParadoxSaveParser.WebAPI; public enum SaveFileProcessingStatus { - Initialized, Uploading, Uploaded, Parsing, SavingResults, Done, Error + Initialized, Uploading, Uploaded, Parsing, SavingResults, Done } public enum Game @@ -22,7 +22,6 @@ public class SaveFileMetadata [JsonConverter(typeof(JsonStringEnumConverter))] public required SaveFileProcessingStatus status { get; set; } - public string? errorMesage { get; set; } private static readonly JsonSerializerOptions _jsonOptions = new() { WriteIndented = true }; public void SaveToFile() diff --git a/TODO.txt b/TODO.txt index 1a4f450..4a8458b 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,10 +1,12 @@ +DTLib.Web: + Add elapsed time to response status log message: `responded 200 (OK) in 0.03 s` + Main: - Move from asp.net to my own http server Add temporary files deletion ParseSaveHandler: - Separate status and error message from metadata Make this method run as background task instead of POST query + Add debug log Parser: Add query support to parse only needed information