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()
{