API methods reimplemented for DTLib.Web

This commit is contained in:
Timerix 2025-03-23 02:49:02 +05:00
parent e3b82d7de5
commit 5d88ad49c0
7 changed files with 79 additions and 91 deletions

View File

@ -23,7 +23,7 @@ namespace ParadoxSaveParser.Lib;
/// ///
/// Console.Write($"| {cur.Value} |"); /// 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.Write($" {next.Value}");
/// Console.WriteLine(); /// Console.WriteLine();
/// } /// }
@ -64,8 +64,8 @@ public class BufferedEnumerator<T> : IEnumerator<LinkedListNode<T>>
return false; return false;
_currentNodeIndex++; _currentNodeIndex++;
_currentNode = _currentNode == null ? _llist.First : _currentNode.Next; _currentNode = _currentNode is null ? _llist.First : _currentNode.Next;
return _currentNode != null; return _currentNode is not null;
} }
public void Reset() public void Reset()

View File

@ -1,7 +1,7 @@
global using System; global using System;
global using System.Collections.Generic;
global using System.IO; global using System.IO;
global using System.Text; global using System.Text;
global using System.Collections.Generic;
namespace ParadoxSaveParser.Lib; namespace ParadoxSaveParser.Lib;
@ -217,7 +217,7 @@ public class Parser
if(!_tokens.MoveNext()) if(!_tokens.MoveNext())
throw new Exception("Unexpected end of file"); throw new Exception("Unexpected end of file");
object? value = ParseValue(); object? value = ParseValue();
if (value == null) if (value is null)
break; break;
list.Add(value); list.Add(value);
} }
@ -277,7 +277,7 @@ public class Parser
throw new UnexpectedTokenException(tok); throw new UnexpectedTokenException(tok);
object? value = ParseValue(); object? value = ParseValue();
if (value == null) if (value is null)
throw new UnexpectedTokenException(_tokens.Current.Value); throw new UnexpectedTokenException(_tokens.Current.Value);
if(!dict.TryGetValue(key, out List<object>? list)) if(!dict.TryGetValue(key, out List<object>? list))

View File

@ -7,7 +7,7 @@ public class Config
public const int ActualVersion = 1; public const int ActualVersion = 1;
public int Version = ActualVersion; 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) public static Config FromDtsod(DtsodV23 d)
{ {

View File

@ -13,7 +13,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="DTLib.Web" Version="1.1.2" /> <PackageReference Include="DTLib.Web" Version="1.2.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -4,7 +4,6 @@ global using System.Text;
global using System.Text.Json; global using System.Text.Json;
global using System.Threading; global using System.Threading;
global using System.Threading.Tasks; global using System.Threading.Tasks;
global using DTLib;
global using DTLib.Demystifier; global using DTLib.Demystifier;
global using DTLib.Filesystem; global using DTLib.Filesystem;
global using DTLib.Logging; global using DTLib.Logging;
@ -30,6 +29,7 @@ public class Program
private static readonly IOPath _configPath = "./config.dtsod"; private static readonly IOPath _configPath = "./config.dtsod";
private static Config _config = new(); private static Config _config = new();
private static readonly ILogger _loggerRoot = new ConsoleLogger(); private static readonly ILogger _loggerRoot = new ConsoleLogger();
private static readonly CancellationTokenSource _mainCancel = new();
private static ConcurrentDictionary<string, SaveFileMetadata> _saveMetadataStorage = new(); private static ConcurrentDictionary<string, SaveFileMetadata> _saveMetadataStorage = new();
private static JsonSerializerOptions _saveSerializerOptions = new() private static JsonSerializerOptions _saveSerializerOptions = new()
@ -39,16 +39,16 @@ public class Program
MaxDepth = 1024, MaxDepth = 1024,
}; };
public static void Main(string[] args) public static void Main(string[] args)
{ {
Console.InputEncoding = Encoding.UTF8; Console.InputEncoding = Encoding.UTF8;
Console.OutputEncoding = Encoding.UTF8; Console.OutputEncoding = Encoding.UTF8;
ContextLogger logger = new ContextLogger(nameof(Main), _loggerRoot); ContextLogger logger = new ContextLogger(nameof(Main), _loggerRoot);
CancellationTokenSource mainCancel = new();
Console.CancelKeyPress += (_, _) => Console.CancelKeyPress += (_, _) =>
{ {
logger.LogInfo("stopping server..."); logger.LogInfo("Ctrl+C Pressed");
mainCancel.Cancel(); _mainCancel.Cancel();
}; };
try try
@ -67,26 +67,11 @@ public class Program
// http server // http server
var router = new SimpleRouter(_loggerRoot); var router = new SimpleRouter(_loggerRoot);
router.DefaultRoute = new ServeFilesRouteHandler("public"); router.DefaultRoute = new ServeFilesRouteHandler("public");
router.MapRoute("/aaa", async ctx => router.MapRoute("/getSaveStatus", HttpMethod.GET, GetSaveStatusHandler);
{ router.MapRoute("/uploadSave/eu4", HttpMethod.POST, UploadSaveHandler);
byte[] buffer = router.MapRoute("/parseSave/eu4", HttpMethod.POST, ParseSaveEU4Handler);
"""
<!DOCTYPE html>
<html>
<body>
<h1>aaa</h1>
</body>
</html>
""".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);
var app = new WebApp(_config.BaseUrl, _loggerRoot, router, mainCancel.Token); var app = new WebApp(_config.BaseUrl, _loggerRoot, router, _mainCancel.Token);
app.Run().GetAwaiter().GetResult(); app.Run().GetAwaiter().GetResult();
} }
catch (OperationCanceledException ex) catch (OperationCanceledException ex)
@ -108,7 +93,8 @@ public class Program
SearchOption.TopDirectoryOnly)) SearchOption.TopDirectoryOnly))
{ {
using var metaFile = File.OpenRead(metaFilePath); using var metaFile = File.OpenRead(metaFilePath);
var meta = JsonSerializer.Deserialize<SaveFileMetadata>(metaFile) ?? throw new NullReferenceException(metaFilePath); var meta = JsonSerializer.Deserialize<SaveFileMetadata>(metaFile) ??
throw new NullReferenceException(metaFilePath);
if (meta.status != SaveFileProcessingStatus.Done) if (meta.status != SaveFileProcessingStatus.Done)
{ {
_loggerRoot.LogWarn(nameof(PrepareLocalFiles), $"metadata file '{metaFilePath}' status has invalid status {meta.status}"); _loggerRoot.LogWarn(nameof(PrepareLocalFiles), $"metadata file '{metaFilePath}' status has invalid status {meta.status}");
@ -119,84 +105,87 @@ public class Program
} }
} }
/* public record ErrorMessage(string errorMessage);
private static async Task UploadSaveHandler(HttpContext httpContext)
private static async Task<HttpStatusCode>ReturnResponse(HttpListenerContext ctx, HttpStatusCode statusCode, object response)
{ {
var remoteFile = httpContext.Request.Form.Files.FirstOrDefault(); await JsonSerializer.SerializeAsync(ctx.Response.OutputStream, response, response.GetType(),
if (remoteFile is null || !remoteFile.FileName.EndsWith(".eu4")) JsonSerializerOptions.Default, _mainCancel.Token);
{ ctx.Response.StatusCode = (int)statusCode;
throw new BadHttpRequestException($"Invalid file format: {remoteFile?.FileName}", return statusCode;
StatusCodes.Status400BadRequest); }
}
private static async Task<HttpStatusCode>ReturnResponse(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 Task<HttpStatusCode>UploadSaveHandler(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(); string saveId = Guid.NewGuid().ToString();
IOPath metaFilePath = PathHelper.GetMetaFilePath(saveId); IOPath metaFilePath = PathHelper.GetMetaFilePath(saveId);
if (File.Exists(metaFilePath)) if (File.Exists(metaFilePath))
{ return await ReturnResponse(ctx, HttpStatusCode.InternalServerError,
httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError; new ErrorMessage($"Guid collision! file' {metaFilePath}' already exists."));
throw new BadHttpRequestException($"Guid collision! file {metaFilePath} already exists.", StatusCodes.Status500InternalServerError);
}
var meta = new SaveFileMetadata { id = saveId, game = Game.EU4, status = SaveFileProcessingStatus.Initialized, }; var meta = new SaveFileMetadata { id = saveId, game = Game.EU4, status = SaveFileProcessingStatus.Initialized, };
if (!_saveMetadataStorage.TryAdd(saveId, meta)) if (!_saveMetadataStorage.TryAdd(saveId, meta))
{ return await ReturnResponse(ctx, HttpStatusCode.InternalServerError,
throw new BadHttpRequestException($"Guid collision! Can't create metadata with id {saveId}", StatusCodes.Status500InternalServerError); new ErrorMessage($"Guid collision! Can't create metadata with id {saveId}"));
}
meta.status = SaveFileProcessingStatus.Uploading; meta.status = SaveFileProcessingStatus.Uploading;
IOPath saveFilePath = PathHelper.GetSaveFilePath(meta.id); IOPath saveFilePath = PathHelper.GetSaveFilePath(meta.id);
await using var saveFile = File.OpenWrite(saveFilePath); await using var saveFile = File.OpenWrite(saveFilePath);
await using var remoteStream = remoteFile.OpenReadStream(); await using var remoteStream = ctx.Request.InputStream;
await remoteStream.CopyToAsync(saveFile); await remoteStream.CopyToAsync(saveFile, _mainCancel.Token);
meta.status = SaveFileProcessingStatus.Uploaded; 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); var ids = ctx.Request.QueryString.GetValues(requestParamName);
string? id = ids.FirstOrDefault(); string? id = ids?.FirstOrDefault();
if (string.IsNullOrEmpty(id)) if (string.IsNullOrEmpty(id))
{ return (null, new ErrorMessage($"No request parameter '{requestParamName}' provided"));
throw new BadHttpRequestException($"No request parameter '{requestParamName}' provided",
StatusCodes.Status400BadRequest);
}
if (!_saveMetadataStorage.TryGetValue(id, out var meta)) if (!_saveMetadataStorage.TryGetValue(id, out var meta))
{ return (null,new ErrorMessage($"Save with {id} not found"));
throw new BadHttpRequestException($"Save with {id} not found",
StatusCodes.Status400BadRequest);
}
return meta; return (meta, null);
} }
private static async Task GetSaveStatusHandler(HttpContext httpContext) private static async Task<HttpStatusCode>GetSaveStatusHandler(HttpListenerContext ctx)
{ {
var meta = GetMetaFromRequestId(httpContext, "id"); var (meta, errorMessage) = GetMetaFromRequestId(ctx, "id");
await httpContext.Response.WriteAsJsonAsync(meta); 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 Task<HttpStatusCode>ParseSaveEU4Handler(HttpListenerContext ctx)
{ {
var meta = GetMetaFromRequestId(httpContext, "id"); var (meta, errorMessage) = GetMetaFromRequestId(ctx, "id");
if (meta.status == SaveFileProcessingStatus.Error) if(errorMessage is not null)
{ return await ReturnResponse(ctx, HttpStatusCode.InternalServerError, errorMessage);
httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
await httpContext.Response.WriteAsJsonAsync(meta);
return;
}
try try
{ {
if (meta.status != SaveFileProcessingStatus.Uploaded) using var zipArchive = ZipFile.Open(PathHelper.GetSaveFilePath(meta!.id).Str, ZipArchiveMode.Read);
throw new Exception($"Invalid save processing status: {meta.status}");
using var zipArchive = ZipFile.Open(PathHelper.GetSaveFilePath(meta.id).Str, ZipArchiveMode.Read);
var zipEntry = zipArchive.Entries.FirstOrDefault(e => e.Name == "gamestate"); var zipEntry = zipArchive.Entries.FirstOrDefault(e => e.Name == "gamestate");
if (zipEntry is null) 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"; string extractedGamestatePath = PathHelper.GetSaveFilePath(meta.id) + ".gamestate";
zipEntry.ExtractToFile(extractedGamestatePath); zipEntry.ExtractToFile(extractedGamestatePath);
var gamestateStream = File.OpenRead(extractedGamestatePath); var gamestateStream = File.OpenRead(extractedGamestatePath);
@ -208,20 +197,18 @@ public class Program
meta.status = SaveFileProcessingStatus.SavingResults; meta.status = SaveFileProcessingStatus.SavingResults;
IOPath resultFilePath = PathHelper.GetParsedSaveFilePath(meta.id); IOPath resultFilePath = PathHelper.GetParsedSaveFilePath(meta.id);
await using var resultFile = File.OpenWrite(resultFilePath); 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.status = SaveFileProcessingStatus.Done;
meta.SaveToFile(); meta.SaveToFile();
} }
catch (Exception ex) catch (Exception ex)
{ {
meta.status = SaveFileProcessingStatus.Error;
string errorMesage = ex.ToStringDemystified(); string errorMesage = ex.ToStringDemystified();
meta.errorMesage = errorMesage; _loggerRoot.LogWarn(nameof(ParseSaveEU4Handler), errorMesage);
httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError; return await ReturnResponse(ctx, HttpStatusCode.BadRequest,
_app.Logger.Log(LogLevel.Error, "ParseSaveEU4 Error: {errorMesage}", errorMesage); new ErrorMessage(errorMesage));
} }
await httpContext.Response.WriteAsJsonAsync(meta); return await ReturnResponse(ctx, HttpStatusCode.OK, meta);
} }
*/
} }

View File

@ -4,7 +4,7 @@ namespace ParadoxSaveParser.WebAPI;
public enum SaveFileProcessingStatus public enum SaveFileProcessingStatus
{ {
Initialized, Uploading, Uploaded, Parsing, SavingResults, Done, Error Initialized, Uploading, Uploaded, Parsing, SavingResults, Done
} }
public enum Game public enum Game
@ -22,7 +22,6 @@ public class SaveFileMetadata
[JsonConverter(typeof(JsonStringEnumConverter))] [JsonConverter(typeof(JsonStringEnumConverter))]
public required SaveFileProcessingStatus status { get; set; } public required SaveFileProcessingStatus status { get; set; }
public string? errorMesage { get; set; }
private static readonly JsonSerializerOptions _jsonOptions = new() { WriteIndented = true }; private static readonly JsonSerializerOptions _jsonOptions = new() { WriteIndented = true };
public void SaveToFile() public void SaveToFile()

View File

@ -1,10 +1,12 @@
DTLib.Web:
Add elapsed time to response status log message: `responded 200 (OK) in 0.03 s`
Main: Main:
Move from asp.net to my own http server
Add temporary files deletion Add temporary files deletion
ParseSaveHandler: ParseSaveHandler:
Separate status and error message from metadata
Make this method run as background task instead of POST query Make this method run as background task instead of POST query
Add debug log
Parser: Parser:
Add query support to parse only needed information Add query support to parse only needed information