From d70b605127c26969c73d4ef59919a0ce2ce69ac7 Mon Sep 17 00:00:00 2001 From: Timerix Date: Sun, 6 Apr 2025 15:37:32 +0500 Subject: [PATCH] refactored responses --- .../ParadoxSaveParser.CLI.csproj | 2 +- .../ParadoxSaveParser.Lib.Tests.csproj | 2 +- .../BackgroundTasks/BackgroundJobManager.cs | 22 ++++ .../BackgroundTasks/SaveParsingOperation.cs | 88 +++++++++++++ .../HttpHelpers/ErrorMessage.cs | 17 +++ .../HttpHelpers/RequestHelper.cs | 17 +++ .../HttpHelpers/ReturnHelper.cs | 70 +++++++++++ .../HttpHelpers/ValueOrError.cs | 19 +++ .../ParadoxSaveParser.WebAPI.csproj | 2 +- ParadoxSaveParser.WebAPI/PathHelper.cs | 10 +- .../Program.HttpHelpers.cs | 97 --------------- .../Program.RequestHandlers.cs | 117 ------------------ ParadoxSaveParser.WebAPI/Program.cs | 107 +++++++++------- .../Routes/GetSaveStatusHandler.cs | 28 +++++ .../Routes/RouteHandlerBase.cs | 16 +++ .../Routes/UploadSaveHandler.cs | 60 +++++++++ ParadoxSaveParser.WebAPI/SaveFileMetadata.cs | 4 + 17 files changed, 415 insertions(+), 263 deletions(-) create mode 100644 ParadoxSaveParser.WebAPI/BackgroundTasks/BackgroundJobManager.cs create mode 100644 ParadoxSaveParser.WebAPI/BackgroundTasks/SaveParsingOperation.cs create mode 100644 ParadoxSaveParser.WebAPI/HttpHelpers/ErrorMessage.cs create mode 100644 ParadoxSaveParser.WebAPI/HttpHelpers/RequestHelper.cs create mode 100644 ParadoxSaveParser.WebAPI/HttpHelpers/ReturnHelper.cs create mode 100644 ParadoxSaveParser.WebAPI/HttpHelpers/ValueOrError.cs delete mode 100644 ParadoxSaveParser.WebAPI/Program.HttpHelpers.cs delete mode 100644 ParadoxSaveParser.WebAPI/Program.RequestHandlers.cs create mode 100644 ParadoxSaveParser.WebAPI/Routes/GetSaveStatusHandler.cs create mode 100644 ParadoxSaveParser.WebAPI/Routes/RouteHandlerBase.cs create mode 100644 ParadoxSaveParser.WebAPI/Routes/UploadSaveHandler.cs diff --git a/ParadoxSaveParser.CLI/ParadoxSaveParser.CLI.csproj b/ParadoxSaveParser.CLI/ParadoxSaveParser.CLI.csproj index 117211a..2dcb490 100644 --- a/ParadoxSaveParser.CLI/ParadoxSaveParser.CLI.csproj +++ b/ParadoxSaveParser.CLI/ParadoxSaveParser.CLI.csproj @@ -14,6 +14,6 @@ - + diff --git a/ParadoxSaveParser.Lib.Tests/ParadoxSaveParser.Lib.Tests.csproj b/ParadoxSaveParser.Lib.Tests/ParadoxSaveParser.Lib.Tests.csproj index 9ff7564..9f81011 100644 --- a/ParadoxSaveParser.Lib.Tests/ParadoxSaveParser.Lib.Tests.csproj +++ b/ParadoxSaveParser.Lib.Tests/ParadoxSaveParser.Lib.Tests.csproj @@ -23,7 +23,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/ParadoxSaveParser.WebAPI/BackgroundTasks/BackgroundJobManager.cs b/ParadoxSaveParser.WebAPI/BackgroundTasks/BackgroundJobManager.cs new file mode 100644 index 0000000..d41995c --- /dev/null +++ b/ParadoxSaveParser.WebAPI/BackgroundTasks/BackgroundJobManager.cs @@ -0,0 +1,22 @@ +namespace ParadoxSaveParser.WebAPI.BackgroundTasks; + +public class BackgroundJobManager +{ + private readonly ILogger _parentLogger; + private long _lastJobId; + + public BackgroundJobManager(ILogger logger) + { + _parentLogger = logger; + } + + public SaveParsingOperation StartNewParsingOperation( + SaveFileMetadata meta, ISearchExpression searchQuery, CancellationToken ct) + { + long nextId = Interlocked.Increment(ref _lastJobId); + var contextLogger = new ContextLogger($"BackgroundJob-{nextId}", _parentLogger); + var op = new SaveParsingOperation(nextId, meta, searchQuery, contextLogger, ct); + op.StartAsync(); + return op; + } +} \ No newline at end of file diff --git a/ParadoxSaveParser.WebAPI/BackgroundTasks/SaveParsingOperation.cs b/ParadoxSaveParser.WebAPI/BackgroundTasks/SaveParsingOperation.cs new file mode 100644 index 0000000..d19e452 --- /dev/null +++ b/ParadoxSaveParser.WebAPI/BackgroundTasks/SaveParsingOperation.cs @@ -0,0 +1,88 @@ +using System.IO.Compression; +using System.Linq; +using System.Text.Encodings.Web; + +namespace ParadoxSaveParser.WebAPI.BackgroundTasks; + +public class SaveParsingOperation +{ + public readonly long OperationId; + public readonly SaveFileMetadata Meta; + public readonly ISearchExpression SearchQuery; + + private readonly ContextLogger _logger; + private readonly CancellationToken _cancelToken; + + private static readonly JsonSerializerOptions _saveSerializerOptions = new() + { + WriteIndented = true, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + MaxDepth = 1024 + }; + + public SaveParsingOperation(long operationId, SaveFileMetadata meta, ISearchExpression searchQuery, + ContextLogger logger, CancellationToken cancelToken) + { + OperationId = operationId; + Meta = meta; + SearchQuery = searchQuery; + _logger = logger; + _cancelToken = cancelToken; + } + + public async void StartAsync() + { + try + { + _logger.LogInfo($"Starting background parsing operation of {Meta.game} save {Meta.id}"); + switch (Meta.game) + { + case Game.EU4: + await ParseSaveEU4(); + break; + default: + throw new ArgumentOutOfRangeException(Meta.game.ToString()); + } + _logger.LogInfo($"Finished parsing operation of {Meta.game} save {Meta.id}"); + } + catch (Exception ex) + { + string errorMesage = ex.ToStringDemystified(); + _logger.LogError(errorMesage); + Meta.errorMessage = errorMesage; + } + finally + { + GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true, true); + } + } + + private async Task ParseSaveEU4() + { + // wait for save file closing + await Task.Delay(200, _cancelToken); + string extractedGamestatePath = PathHelper.GetSaveFilePath(Meta.id) + ".gamestate"; + 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"); + + zipEntry.ExtractToFile(extractedGamestatePath, true); + } + + var gamestateStream = File.OpenRead(extractedGamestatePath); + + Meta.status = SaveFileProcessingStatus.Parsing; + var parser = new SaveParserEU4(gamestateStream, SearchQuery); + var result = parser.Parse(); + + Meta.status = SaveFileProcessingStatus.SavingResults; + var resultFilePath = PathHelper.GetParsedSaveFilePath(Meta.id); + await using var resultFile = File.OpenWrite(resultFilePath); + await JsonSerializer.SerializeAsync(resultFile, result, + _saveSerializerOptions, _cancelToken); + Meta.status = SaveFileProcessingStatus.Done; + Meta.SaveToFile(); + } +} \ No newline at end of file diff --git a/ParadoxSaveParser.WebAPI/HttpHelpers/ErrorMessage.cs b/ParadoxSaveParser.WebAPI/HttpHelpers/ErrorMessage.cs new file mode 100644 index 0000000..938982c --- /dev/null +++ b/ParadoxSaveParser.WebAPI/HttpHelpers/ErrorMessage.cs @@ -0,0 +1,17 @@ +using System.Net; +using System.Text.Json.Serialization; + +namespace ParadoxSaveParser.WebAPI.HttpHelpers; + +public record ErrorMessage +{ + public ErrorMessage(HttpStatusCode statusCode, string message) + { + StatusCode = statusCode; + Message = message; + } + + [JsonIgnore] public HttpStatusCode StatusCode { get; } + + [JsonPropertyName("errorMessage")] public string Message { get; } +} \ No newline at end of file diff --git a/ParadoxSaveParser.WebAPI/HttpHelpers/RequestHelper.cs b/ParadoxSaveParser.WebAPI/HttpHelpers/RequestHelper.cs new file mode 100644 index 0000000..a6cfbf0 --- /dev/null +++ b/ParadoxSaveParser.WebAPI/HttpHelpers/RequestHelper.cs @@ -0,0 +1,17 @@ +using System.Linq; +using System.Net; + +namespace ParadoxSaveParser.WebAPI.HttpHelpers; + +public class RequestHelper +{ + internal static ValueOrError GetQueryValue(HttpListenerContext ctx, string paramName) + { + string[]? values = ctx.Request.QueryString.GetValues(paramName); + string? value = values?.FirstOrDefault(); + if (string.IsNullOrEmpty(value)) + return new ErrorMessage(HttpStatusCode.BadRequest, + $"No request parameter '{paramName}' provided"); + return value; + } +} \ No newline at end of file diff --git a/ParadoxSaveParser.WebAPI/HttpHelpers/ReturnHelper.cs b/ParadoxSaveParser.WebAPI/HttpHelpers/ReturnHelper.cs new file mode 100644 index 0000000..369e52f --- /dev/null +++ b/ParadoxSaveParser.WebAPI/HttpHelpers/ReturnHelper.cs @@ -0,0 +1,70 @@ +using System.Net; +using System.Text.Encodings.Web; +using DTLib.Extensions; + +namespace ParadoxSaveParser.WebAPI.HttpHelpers; + +public class ReturnHelper +{ + private static readonly JsonSerializerOptions _responseJsonSerializerOptions = new() + { + WriteIndented = false, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + MaxDepth = 1024 + }; + + private static async Task ResponseShort(HttpListenerContext ctx, + ContextLogger logger, + CancellationToken ct, + byte[] value, + HttpStatusCode statusCode, + string contentType) + { + ctx.Response.StatusCode = (int)statusCode; + ctx.Response.ContentType = contentType; + logger.LogDebug($"response (length: {value.Length} type: {contentType})"); + if (value.Length > 4000) + { + logger.LogWarn($"response (length: {value.Length} type: {contentType})\n" + + $"Content is too big for {nameof(ResponseShort)}." + + $"You should send stream instead of byte array."); + } + await ctx.Response.OutputStream.WriteAsync(value, ct); + } + + public static async Task ResponseString( + HttpListenerContext ctx, + ContextLogger logger, + CancellationToken ct, + string value, + HttpStatusCode statusCode = HttpStatusCode.OK, + string contentType = "text/plain") + { + await ResponseShort(ctx, logger, ct, value.ToBytes(), statusCode, contentType); + logger.LogDebug(value); + return statusCode; + } + + public static async Task ResponseJson( + HttpListenerContext ctx, + ContextLogger logger, + CancellationToken ct, + object value, + HttpStatusCode statusCode = HttpStatusCode.OK) + { + string json = JsonSerializer.Serialize( + value, + value.GetType(), + _responseJsonSerializerOptions); + return await ResponseString(ctx, logger, ct, json, statusCode, "application/json"); + } + + public static async Task ResponseError( + HttpListenerContext ctx, + ContextLogger logger, + CancellationToken ct, + ErrorMessage error) + { + return await ResponseJson(ctx, logger, ct, error, error.StatusCode); + } +} \ No newline at end of file diff --git a/ParadoxSaveParser.WebAPI/HttpHelpers/ValueOrError.cs b/ParadoxSaveParser.WebAPI/HttpHelpers/ValueOrError.cs new file mode 100644 index 0000000..1888e7e --- /dev/null +++ b/ParadoxSaveParser.WebAPI/HttpHelpers/ValueOrError.cs @@ -0,0 +1,19 @@ +namespace ParadoxSaveParser.WebAPI.HttpHelpers; + +public class ValueOrError + { + public readonly ErrorMessage? Error; + public readonly T? Value; + + private ValueOrError(T? value, ErrorMessage? error) + { + Value = value; + Error = error; + } + + public bool HasError => Error is not null; + + public static implicit operator ValueOrError(T v) => new(v, null); + + public static implicit operator ValueOrError(ErrorMessage e) => new(default, e); + } \ No newline at end of file diff --git a/ParadoxSaveParser.WebAPI/ParadoxSaveParser.WebAPI.csproj b/ParadoxSaveParser.WebAPI/ParadoxSaveParser.WebAPI.csproj index 4de0743..1700ba8 100644 --- a/ParadoxSaveParser.WebAPI/ParadoxSaveParser.WebAPI.csproj +++ b/ParadoxSaveParser.WebAPI/ParadoxSaveParser.WebAPI.csproj @@ -13,6 +13,6 @@ - + diff --git a/ParadoxSaveParser.WebAPI/PathHelper.cs b/ParadoxSaveParser.WebAPI/PathHelper.cs index b6a7e0e..ee55f52 100644 --- a/ParadoxSaveParser.WebAPI/PathHelper.cs +++ b/ParadoxSaveParser.WebAPI/PathHelper.cs @@ -1,4 +1,6 @@ -namespace ParadoxSaveParser.WebAPI; +using System.IO; + +namespace ParadoxSaveParser.WebAPI; public static class PathHelper { @@ -10,4 +12,10 @@ public static class PathHelper public static IOPath GetSaveFilePath(string save_id) => Path.Concat(SAVES_DIR, save_id + ".eu4"); public static IOPath GetParsedSaveFilePath(string save_id) => Path.Concat(SAVES_DIR, save_id + ".parsed.json"); + + public static void CreateProgramDirectories() + { + Directory.Create(DATA_DIR); + Directory.Create(SAVES_DIR); + } } \ No newline at end of file diff --git a/ParadoxSaveParser.WebAPI/Program.HttpHelpers.cs b/ParadoxSaveParser.WebAPI/Program.HttpHelpers.cs deleted file mode 100644 index a46c3bc..0000000 --- a/ParadoxSaveParser.WebAPI/Program.HttpHelpers.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System.Linq; -using System.Net; -using System.Text.Encodings.Web; -using System.Text.Json.Serialization; -using DTLib.Extensions; - -namespace ParadoxSaveParser.WebAPI; - -public partial class Program -{ - private static readonly JsonSerializerOptions _responseJsonSerializerOptions = new() - { - WriteIndented = false, - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, - MaxDepth = 1024 - }; - - public static async Task ReturnResponseString(HttpListenerContext ctx, - string value, HttpStatusCode statusCode = HttpStatusCode.OK) - { - ctx.Response.StatusCode = (int)statusCode; - ctx.Response.ContentType = "text/plain"; - await ctx.Response.OutputStream.WriteAsync( - value.ToBytes(), - _mainCancel.Token); - return statusCode; - } - - public static async Task ReturnResponseJson(HttpListenerContext ctx, - object value, HttpStatusCode statusCode = HttpStatusCode.OK) - { - ctx.Response.StatusCode = (int)statusCode; - ctx.Response.ContentType = "application/json"; - await JsonSerializer.SerializeAsync( - ctx.Response.OutputStream, - value, - value.GetType(), - _responseJsonSerializerOptions, - _mainCancel.Token); - return statusCode; - } - - public static async Task ReturnResponseError(HttpListenerContext ctx, ErrorMessage error) - { - ctx.Response.StatusCode = (int)error.StatusCode; - ctx.Response.ContentType = "application/json"; - await JsonSerializer.SerializeAsync( - ctx.Response.OutputStream, - error, - typeof(ErrorMessage), - _responseJsonSerializerOptions, - _mainCancel.Token); - return error.StatusCode; - } - - - internal static ValueOrError GetRequestQueryValue(HttpListenerContext ctx, string paramName) - { - string[]? values = ctx.Request.QueryString.GetValues(paramName); - string? value = values?.FirstOrDefault(); - if (string.IsNullOrEmpty(value)) - return new ErrorMessage(HttpStatusCode.BadRequest, - $"No request parameter '{paramName}' provided"); - return value; - } - - public record ErrorMessage - { - public ErrorMessage(HttpStatusCode statusCode, string message) - { - StatusCode = statusCode; - Message = message; - } - - [JsonIgnore] public HttpStatusCode StatusCode { get; } - - [JsonPropertyName("errorMessage")] public string Message { get; } - } - - public class ValueOrError - { - public readonly ErrorMessage? Error; - public readonly T? Value; - - private ValueOrError(T? value, ErrorMessage? error) - { - Value = value; - Error = error; - } - - public bool HasError => Error is not null; - - public static implicit operator ValueOrError(T v) => new(v, null); - - public static implicit operator ValueOrError(ErrorMessage e) => new(default, e); - } -} \ No newline at end of file diff --git a/ParadoxSaveParser.WebAPI/Program.RequestHandlers.cs b/ParadoxSaveParser.WebAPI/Program.RequestHandlers.cs deleted file mode 100644 index 373b5c4..0000000 --- a/ParadoxSaveParser.WebAPI/Program.RequestHandlers.cs +++ /dev/null @@ -1,117 +0,0 @@ -using System.IO.Compression; -using System.Linq; -using System.Net; - -namespace ParadoxSaveParser.WebAPI; - -public partial class Program -{ - private static async Task UploadSaveHandler(HttpListenerContext ctx) - { - string? contentType = ctx.Request.Headers.GetValues("Content-Type")?.FirstOrDefault(); - if (contentType != "application/octet-stream") - return await ReturnResponseError(ctx, new ErrorMessage( - HttpStatusCode.BadRequest, - $"Invalid request Content-Type: '{contentType}'")); - - string saveId = Guid.NewGuid().ToString(); - var metaFilePath = PathHelper.GetMetaFilePath(saveId); - if (File.Exists(metaFilePath)) - return await ReturnResponseError(ctx, new ErrorMessage( - HttpStatusCode.InternalServerError, - $"Guid collision! file' {metaFilePath}' already exists.")); - - var meta = new SaveFileMetadata - { id = saveId, game = Game.EU4, status = SaveFileProcessingStatus.Initialized }; - if (!_saveMetadataStorage.TryAdd(saveId, meta)) - return await ReturnResponseError(ctx, new ErrorMessage( - HttpStatusCode.InternalServerError, - $"Guid collision! Can't create metadata with id {saveId}")); - - meta.status = SaveFileProcessingStatus.Uploading; - var saveFilePath = PathHelper.GetSaveFilePath(meta.id); - await using var saveFile = File.OpenWrite(saveFilePath); - await using var remoteStream = ctx.Request.InputStream; - await remoteStream.CopyToAsync(saveFile, _mainCancel.Token); - meta.status = SaveFileProcessingStatus.Uploaded; - - return await ReturnResponseString(ctx, saveId); - } - - private static ValueOrError GetMetaFromRequestId(HttpListenerContext ctx, - string requestParamName) - { - var idOrError = GetRequestQueryValue(ctx, requestParamName); - if (idOrError.HasError) - return idOrError.Error!; - - if (!_saveMetadataStorage.TryGetValue(idOrError.Value!, out var meta)) - return new ErrorMessage(HttpStatusCode.InternalServerError, - $"Save with id {idOrError.Value} not found"); - - return meta; - } - - private static async Task GetSaveStatusHandler(HttpListenerContext ctx) - { - var metaOrError = GetMetaFromRequestId(ctx, "id"); - if (metaOrError.HasError) - return await ReturnResponseError(ctx, metaOrError.Error!); - - return await ReturnResponseJson(ctx, metaOrError.Value!); - } - - private static async Task ParseSaveEU4Handler(HttpListenerContext ctx) - { - var metaOrError = GetMetaFromRequestId(ctx, "id"); - if (metaOrError.HasError) - return await ReturnResponseError(ctx, metaOrError.Error!); - var meta = metaOrError.Value!; - - var searchQueryOrError = GetRequestQueryValue(ctx, "search"); - if (searchQueryOrError.HasError) - return await ReturnResponseError(ctx, searchQueryOrError.Error!); - string searchQuery = searchQueryOrError.Value!; - - try - { - string extractedGamestatePath = PathHelper.GetSaveFilePath(meta.id) + ".gamestate"; - 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) - return await ReturnResponseError(ctx, new ErrorMessage( - HttpStatusCode.BadRequest, - "Invalid save format: no 'gamestate' file found")); - - zipEntry.ExtractToFile(extractedGamestatePath, true); - } - - var gamestateStream = File.OpenRead(extractedGamestatePath); - - meta.status = SaveFileProcessingStatus.Parsing; - var se = SearchExpressionCompiler.Compile(searchQuery); - var parser = new SaveParserEU4(gamestateStream, se); - var result = parser.Parse(); - - meta.status = SaveFileProcessingStatus.SavingResults; - var resultFilePath = PathHelper.GetParsedSaveFilePath(meta.id); - await using var resultFile = File.OpenWrite(resultFilePath); - await JsonSerializer.SerializeAsync(resultFile, result, _saveSerializerOptions, _mainCancel.Token); - meta.status = SaveFileProcessingStatus.Done; - meta.SaveToFile(); - } - catch (Exception ex) - { - string errorMesage = ex.ToStringDemystified(); - _loggerRoot.LogWarn(nameof(ParseSaveEU4Handler), errorMesage); - return await ReturnResponseError(ctx, new ErrorMessage(HttpStatusCode.BadRequest, errorMesage)); - } - finally - { - GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true, true); - } - - return await ReturnResponseJson(ctx, meta); - } -} \ No newline at end of file diff --git a/ParadoxSaveParser.WebAPI/Program.cs b/ParadoxSaveParser.WebAPI/Program.cs index ace24e0..64c0d37 100644 --- a/ParadoxSaveParser.WebAPI/Program.cs +++ b/ParadoxSaveParser.WebAPI/Program.cs @@ -13,42 +13,56 @@ global using File = DTLib.Filesystem.File; global using Path = DTLib.Filesystem.Path; using System.Collections.Concurrent; using System.IO; -using System.Text.Encodings.Web; +using DTLib.Console; using DTLib.Dtsod; using DTLib.Web; using DTLib.Web.Routes; +using ParadoxSaveParser.WebAPI.BackgroundTasks; +using ParadoxSaveParser.WebAPI.Routes; namespace ParadoxSaveParser.WebAPI; -public static partial class Program +public static class Program { private static readonly IOPath _configPath = "./config.dtsod"; private static Config _config = new(); - - private static readonly ILogger _loggerRoot = new CompositeLogger( - new ConsoleLogger(), - new FileLogger("logs", "ParadoxSaveParser.WebAPI")); - + private static bool IsDebug = true; private static readonly CancellationTokenSource _mainCancel = new(); - private static readonly ConcurrentDictionary _saveMetadataStorage = new(); + internal static readonly ConcurrentDictionary _saveMetadataStorage = new(); - private static readonly JsonSerializerOptions _saveSerializerOptions = new() - { - WriteIndented = true, - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, - MaxDepth = 1024 - }; public static void Main(string[] args) { Console.InputEncoding = Encoding.UTF8; Console.OutputEncoding = Encoding.UTF8; Console.CursorVisible = false; - var logger = new ContextLogger(nameof(Main), _loggerRoot); + + #if DEBUG + IsDebug = true; + #endif + + new LaunchArgumentParser( + new LaunchArgument(["-d", "--debug"], + "enables debug log output to console", + () => IsDebug = true) + ).AllowNoArguments().ParseAndHandle(args); + + var loggerRoot = new CompositeLogger( + new ConsoleLogger + { + DebugLogEnabled = IsDebug + }, + new FileLogger("logs", "ParadoxSaveParser.WebAPI") + { + DebugLogEnabled = true + }); + var loggerMain = new ContextLogger(nameof(Main), loggerRoot); + loggerMain.LogDebug("Debug log is enabled"); + Console.CancelKeyPress += (_, e) => { e.Cancel = true; - logger.LogInfo("Ctrl+C Pressed"); + loggerMain.LogInfo("Ctrl+C Pressed"); _mainCancel.Cancel(); }; @@ -57,54 +71,57 @@ public static partial class Program // config if (!File.Exists(_configPath)) { - logger.LogWarn("config file not found."); + loggerMain.LogWarn("config file not found."); File.WriteAllText(_configPath, _config.ToString()); - logger.LogWarn($"created default at {_configPath}."); + loggerMain.LogWarn($"created default at {_configPath}."); } else { _config = Config.FromDtsod(new DtsodV23(File.ReadAllText(_configPath))); } - PrepareLocalFiles(); + PathHelper.CreateProgramDirectories(); + var metaFiles = System.IO.Directory.GetFiles( + PathHelper.SAVES_DIR.Str, "*.meta.json", + SearchOption.TopDirectoryOnly); + foreach (string metaFilePath in metaFiles) + { + using var metaFile = File.OpenRead(metaFilePath); + var meta = JsonSerializer.Deserialize(metaFile) ?? + throw new NullReferenceException(metaFilePath); + if (meta.status != SaveFileProcessingStatus.Done) + loggerMain.LogWarn(nameof(Main), + $"metadata file '{metaFilePath}' status has invalid status {meta.status}"); + + if (!_saveMetadataStorage.TryAdd(meta.id, meta)) + throw new Exception("Guid collision!"); + } + + var bgJobManager = new BackgroundJobManager(loggerRoot); + var saveParsingSearchExpressions = new Dictionary + { + { Game.EU4, SearchExpressionCompiler.Compile("*.~") }, + }; + // http server - var router = new SimpleRouter(_loggerRoot); + var router = new SimpleRouter(loggerRoot); router.DefaultRoute = new ServeFilesRouteHandler("public"); - router.MapRoute("/getSaveStatus", HttpMethod.GET, GetSaveStatusHandler); - router.MapRoute("/uploadSave/eu4", HttpMethod.POST, UploadSaveHandler); - router.MapRoute("/parseSave/eu4", HttpMethod.POST, ParseSaveEU4Handler); + router.MapRoute("/getSaveStatus", HttpMethod.GET, new GetSaveStatusHandler(_mainCancel.Token)); + router.MapRoute("/uploadSave/eu4", HttpMethod.POST, new UploadSaveHandler(_mainCancel.Token, bgJobManager, saveParsingSearchExpressions)); - 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) { - logger.LogWarn($"catched OperationCanceledException from {ex.Source}"); + loggerMain.LogWarn($"catched OperationCanceledException from {ex.Source}"); } catch (Exception ex) { - logger.LogError(ex.ToStringDemystified()); + loggerMain.LogError(ex.ToStringDemystified()); } } - public static void PrepareLocalFiles() - { - Directory.Create(PathHelper.DATA_DIR); - Directory.Create(PathHelper.SAVES_DIR); - foreach (string metaFilePath in System.IO.Directory.GetFiles( - PathHelper.SAVES_DIR.Str, "*.meta.json", - SearchOption.TopDirectoryOnly)) - { - using var metaFile = File.OpenRead(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}"); - - if (!_saveMetadataStorage.TryAdd(meta.id, meta)) - throw new Exception("Guid collision!"); - } - } + } \ No newline at end of file diff --git a/ParadoxSaveParser.WebAPI/Routes/GetSaveStatusHandler.cs b/ParadoxSaveParser.WebAPI/Routes/GetSaveStatusHandler.cs new file mode 100644 index 0000000..de72635 --- /dev/null +++ b/ParadoxSaveParser.WebAPI/Routes/GetSaveStatusHandler.cs @@ -0,0 +1,28 @@ +using System.Net; +using ParadoxSaveParser.WebAPI.HttpHelpers; + +namespace ParadoxSaveParser.WebAPI.Routes; + +internal class GetSaveStatusHandler : RouteHandlerBase +{ + public GetSaveStatusHandler(CancellationToken cancelAllToken) + : base(cancelAllToken) + { + } + + public override async Task HandleRequest( + HttpListenerContext ctx, ContextLogger requestLogger) + { + var idOrError = RequestHelper.GetQueryValue(ctx, "id"); + if (idOrError.HasError) + return await ReturnHelper.ResponseError(ctx, requestLogger, _cancelAllToken, idOrError.Error!); + + if (!Program._saveMetadataStorage.TryGetValue(idOrError.Value!, out var meta)) + return await ReturnHelper.ResponseError(ctx, requestLogger, _cancelAllToken, + new ErrorMessage(HttpStatusCode.InternalServerError, + $"Save with id {idOrError.Value} not found") + ); + + return await ReturnHelper.ResponseJson(ctx, requestLogger, _cancelAllToken, meta); + } +} \ No newline at end of file diff --git a/ParadoxSaveParser.WebAPI/Routes/RouteHandlerBase.cs b/ParadoxSaveParser.WebAPI/Routes/RouteHandlerBase.cs new file mode 100644 index 0000000..33defb4 --- /dev/null +++ b/ParadoxSaveParser.WebAPI/Routes/RouteHandlerBase.cs @@ -0,0 +1,16 @@ +using System.Net; +using DTLib.Web.Routes; + +namespace ParadoxSaveParser.WebAPI.Routes; + +public abstract class RouteHandlerBase : IRouteHandler +{ + protected readonly CancellationToken _cancelAllToken; + + protected RouteHandlerBase(CancellationToken cancelAllToken) + { + _cancelAllToken = cancelAllToken; + } + + public abstract Task HandleRequest(HttpListenerContext ctx, ContextLogger requestLogger); +} \ No newline at end of file diff --git a/ParadoxSaveParser.WebAPI/Routes/UploadSaveHandler.cs b/ParadoxSaveParser.WebAPI/Routes/UploadSaveHandler.cs new file mode 100644 index 0000000..dd68831 --- /dev/null +++ b/ParadoxSaveParser.WebAPI/Routes/UploadSaveHandler.cs @@ -0,0 +1,60 @@ +using System.Linq; +using System.Net; +using ParadoxSaveParser.WebAPI.BackgroundTasks; +using ParadoxSaveParser.WebAPI.HttpHelpers; + +namespace ParadoxSaveParser.WebAPI.Routes; + +public class UploadSaveHandler : RouteHandlerBase +{ + private readonly BackgroundJobManager _bgJobManager; + private readonly Dictionary _searchQueries; + + public UploadSaveHandler( + CancellationToken cancelAllToken, + BackgroundJobManager bgJobManager, + Dictionary searchQueries) + : base(cancelAllToken) + { + _bgJobManager = bgJobManager; + _searchQueries = searchQueries; + } + + public override async Task HandleRequest( + HttpListenerContext ctx, ContextLogger requestLogger) + { + string? contentType = ctx.Request.Headers.GetValues("Content-Type")?.FirstOrDefault(); + if (contentType != "application/octet-stream") + return await ReturnHelper.ResponseError(ctx, requestLogger, _cancelAllToken, + new ErrorMessage(HttpStatusCode.BadRequest, + $"Invalid request Content-Type: '{contentType}'") + ); + + string saveId = Guid.NewGuid().ToString(); + var metaFilePath = PathHelper.GetMetaFilePath(saveId); + if (File.Exists(metaFilePath)) + return await ReturnHelper.ResponseError(ctx, requestLogger, _cancelAllToken, + new ErrorMessage(HttpStatusCode.InternalServerError, + $"Guid collision! file' {metaFilePath}' already exists.") + ); + + var meta = new SaveFileMetadata + { id = saveId, game = Game.EU4, status = SaveFileProcessingStatus.Initialized }; + if (!Program._saveMetadataStorage.TryAdd(saveId, meta)) + return await ReturnHelper.ResponseError(ctx, requestLogger, _cancelAllToken, + new ErrorMessage(HttpStatusCode.InternalServerError, + $"Guid collision! Can't create metadata with id {saveId}") + ); + + meta.status = SaveFileProcessingStatus.Uploading; + var saveFilePath = PathHelper.GetSaveFilePath(meta.id); + await using var saveFile = File.OpenWrite(saveFilePath); + await using var remoteStream = ctx.Request.InputStream; + await remoteStream.CopyToAsync(saveFile, _cancelAllToken); + meta.status = SaveFileProcessingStatus.Uploaded; + + _bgJobManager.StartNewParsingOperation(meta, _searchQueries[meta.game], _cancelAllToken); + dynamic responseData = new { saveId }; + return await ReturnHelper.ResponseJson(ctx, requestLogger, _cancelAllToken, responseData); + } +} \ No newline at end of file diff --git a/ParadoxSaveParser.WebAPI/SaveFileMetadata.cs b/ParadoxSaveParser.WebAPI/SaveFileMetadata.cs index 22061db..59e349a 100644 --- a/ParadoxSaveParser.WebAPI/SaveFileMetadata.cs +++ b/ParadoxSaveParser.WebAPI/SaveFileMetadata.cs @@ -28,6 +28,10 @@ public class SaveFileMetadata [JsonConverter(typeof(JsonStringEnumConverter))] public required SaveFileProcessingStatus status { get; set; } + + // if error occured during parsing, it's message is saved here + // status stays the same as when error occured + public string? errorMessage { get; set; } public void SaveToFile() {