diff --git a/ParadoxSaveParser.Lib/ParadoxSaveParser.Lib.csproj b/ParadoxSaveParser.Lib/ParadoxSaveParser.Lib.csproj index 1b4b7a1..45b3a2a 100644 --- a/ParadoxSaveParser.Lib/ParadoxSaveParser.Lib.csproj +++ b/ParadoxSaveParser.Lib/ParadoxSaveParser.Lib.csproj @@ -8,6 +8,6 @@ - + diff --git a/ParadoxSaveParser.WebAPI/BackgroundTasks/BackgroundJobManager.cs b/ParadoxSaveParser.WebAPI/BackgroundTasks/BackgroundJobManager.cs index 1723add..8f0ce0a 100644 --- a/ParadoxSaveParser.WebAPI/BackgroundTasks/BackgroundJobManager.cs +++ b/ParadoxSaveParser.WebAPI/BackgroundTasks/BackgroundJobManager.cs @@ -1,4 +1,5 @@ -using ParadoxSaveParser.WebAPI.SaveDataFilters; +using ParadoxSaveParser.WebAPI.Database; +using ParadoxSaveParser.WebAPI.SaveDataFilters; namespace ParadoxSaveParser.WebAPI.BackgroundTasks; diff --git a/ParadoxSaveParser.WebAPI/BackgroundTasks/SaveParsingOperation.cs b/ParadoxSaveParser.WebAPI/BackgroundTasks/SaveParsingOperation.cs index 93e8571..27692d7 100644 --- a/ParadoxSaveParser.WebAPI/BackgroundTasks/SaveParsingOperation.cs +++ b/ParadoxSaveParser.WebAPI/BackgroundTasks/SaveParsingOperation.cs @@ -1,7 +1,9 @@ -using System.IO.Compression; +using System.IO; +using System.IO.Compression; using System.Linq; using System.Text.Encodings.Web; using System.Text.Json.Serialization; +using ParadoxSaveParser.WebAPI.Database; using ParadoxSaveParser.WebAPI.SaveDataFilters; namespace ParadoxSaveParser.WebAPI.BackgroundTasks; @@ -13,9 +15,9 @@ public class SaveParsingOperation private readonly SaveFileMetadata _meta; private readonly ISaveDataFilter _filter; private readonly ContextLogger _logger; - private readonly CancellationToken _cancelToken; + private readonly CancellationToken _ct; - private static readonly JsonSerializerOptions _saveSerializerOptions = new() + private static readonly JsonSerializerOptions _jsonOptions = new() { WriteIndented = false, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, @@ -24,13 +26,13 @@ public class SaveParsingOperation }; public SaveParsingOperation(long operationId, SaveFileMetadata meta, ISaveDataFilter filter, - ContextLogger logger, CancellationToken cancelToken) + ContextLogger logger, CancellationToken ct) { OperationId = operationId; _meta = meta; _filter = filter; _logger = logger; - _cancelToken = cancelToken; + _ct = ct; } public async void StartAsync() @@ -52,7 +54,14 @@ public class SaveParsingOperation { string errorMesage = ex.ToStringDemystified(); _logger.LogError(errorMesage); - _meta.errorMessage = errorMesage; + try + { + await Program.DB.SetMetadataError(_meta, errorMesage); + } + catch (Exception ex2) + { + _logger.LogError(ex2); + } } finally { @@ -63,30 +72,31 @@ public class SaveParsingOperation 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)) + await Task.Delay(200, _ct); + Dictionary parsedData; + await using (var gamestateStream = PathHelper.CreateTempFile()) { - var zipEntry = zipArchive.Entries.FirstOrDefault(e => e.Name == "gamestate"); - if (zipEntry is null) - throw new Exception("Invalid save format: no 'gamestate' file found"); + using (var zipArchive = ZipFile.Open(_meta.GetSaveFilePath().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); + await zipEntry.Open().CopyToAsync(gamestateStream, _ct); + gamestateStream.Seek(0, SeekOrigin.Begin); + } + + await Program.DB.UpdateMetadataStatus(_meta, SaveFileProcessingStatus.Parsing); + var parser = new SaveParserEU4(gamestateStream, _filter.SearchExpression); + parsedData = parser.Parse(); } - var gamestateStream = File.OpenRead(extractedGamestatePath); + _filter.Apply(parsedData); - _meta.status = SaveFileProcessingStatus.Parsing; - var parser = new SaveParserEU4(gamestateStream, _filter.SearchExpression); - var result = parser.Parse(); - _filter.Apply(result); - - _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(); + await Program.DB.UpdateMetadataStatus(_meta, SaveFileProcessingStatus.SavingResults); + var resultFilePath = _meta.GetParsedDataPath(); + await using (var resultFile = File.OpenWrite(resultFilePath)) + await JsonSerializer.SerializeAsync(resultFile, parsedData, _jsonOptions, _ct); + await Program.DB.UpdateMetadataStatus(_meta, SaveFileProcessingStatus.Done); } } \ No newline at end of file diff --git a/ParadoxSaveParser.WebAPI/Database/DatabaseConnector.cs b/ParadoxSaveParser.WebAPI/Database/DatabaseConnector.cs new file mode 100644 index 0000000..515ff74 --- /dev/null +++ b/ParadoxSaveParser.WebAPI/Database/DatabaseConnector.cs @@ -0,0 +1,105 @@ +using System.Linq; +using SQLite; + +namespace ParadoxSaveParser.WebAPI.Database; + +public class DatabaseConnector +{ + private readonly int _uploadsLifetimeDays; + private readonly SQLiteAsyncConnection _db; + private readonly ContextLogger _logger; + + public DatabaseConnector(string databasePath, int uploadsLifetimeDays, ILogger logger) + { + _uploadsLifetimeDays = uploadsLifetimeDays; + _db = new SQLiteAsyncConnection(databasePath); + _logger = new ContextLogger("Database", logger); + } + + public async Task InitializeAsync() + { + await _db.CreateTableAsync(); + } + + public async Task GetMetadata(string id) + { + return (await _db.QueryAsync( + "select * from SaveFileMetadata where id = ?", id)) + .FirstOrDefault(); + } + + public async Task CreateMetadata(Game game) + { + var meta = new SaveFileMetadata + { + id = Guid.NewGuid().ToString(), + game = game, + status = SaveFileProcessingStatus.Initialized, + uploadDateTime = DateTime.Now + }; + await _db.InsertAsync(meta); + return meta; + } + + public async Task UpdateMetadataStatus(SaveFileMetadata meta, SaveFileProcessingStatus status) + { + meta.status = status; + await _db.UpdateAsync(meta); + } + + public async Task SetMetadataError(SaveFileMetadata meta, string errorMessage) + { + meta.errorMessage = errorMessage; + await UpdateMetadataStatus(meta, SaveFileProcessingStatus.Error); + DeleteAssociatedFiles(meta); + } + + public async Task DeleteMetadata(SaveFileMetadata meta) + { + _logger.LogDebug($"Deleting save file (id: {meta.id} status: {meta.status} " + + $"uploadDate: {meta.uploadDateTime:yyyy/MM/dd})"); + await _db.DeleteAsync(meta); + DeleteAssociatedFiles(meta); + } + + /// + /// WARNING: Call this method only when no parsing operations are running. + /// + public async Task DeleteInvalidData() + { + _logger.LogInfo("Deleting invalid data..."); + + var expirationDate = DateTime.Now.AddDays(-_uploadsLifetimeDays); + var metadataTable = _db.Table(); + int rowCount = await metadataTable.CountAsync(); + int i = 0; + int deleteCount = 0; + while(i < rowCount) + { + var meta = await metadataTable.ElementAtAsync(i)!; + if(meta.status != SaveFileProcessingStatus.Done || + meta.uploadDateTime < expirationDate || + !meta.AssociatedFilesExist()) + { + deleteCount++; + await DeleteMetadata(meta); + } + i++; + } + + _logger.LogInfo($"Deleted {deleteCount} invalid records"); + } + + private void TryDeleteFile(IOPath file) + { + if (!File.Exists(file)) return; + _logger.LogDebug($"Deleting file '{file}'"); + File.Delete(file); + } + + private void DeleteAssociatedFiles(SaveFileMetadata meta) + { + TryDeleteFile(meta.GetSaveFilePath()); + TryDeleteFile(meta.GetParsedDataPath()); + } +} \ No newline at end of file diff --git a/ParadoxSaveParser.WebAPI/Database/SaveFileMetadata.cs b/ParadoxSaveParser.WebAPI/Database/SaveFileMetadata.cs new file mode 100644 index 0000000..0a6a635 --- /dev/null +++ b/ParadoxSaveParser.WebAPI/Database/SaveFileMetadata.cs @@ -0,0 +1,45 @@ +using System.Text.Json.Serialization; + +namespace ParadoxSaveParser.WebAPI.Database; + +public enum SaveFileProcessingStatus +{ + Initialized, + Uploaded, + Parsing, + SavingResults, + Done, + Error, +} + +public class SaveFileMetadata +{ + [SQLite.PrimaryKey] + [SQLite.NotNull] + public string id { get; init; } = null!; + + [JsonConverter(typeof(JsonStringEnumConverter))] + [SQLite.NotNull] + public Game game { get; init; } + + [JsonConverter(typeof(JsonStringEnumConverter))] + [SQLite.NotNull] + public SaveFileProcessingStatus status { get; set; } + + [SQLite.NotNull] + public DateTime uploadDateTime { 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 IOPath GetSaveFilePath() => Path.Concat(PathHelper.SAVES_DIR, id + ".eu4"); + + public IOPath GetParsedDataPath() => Path.Concat(PathHelper.PARSED_DIR, id + ".parsed.json"); + + public bool AssociatedFilesExist() + { + return File.Exists(GetSaveFilePath()) && File.Exists(GetParsedDataPath()); + } +} \ No newline at end of file diff --git a/ParadoxSaveParser.WebAPI/Game.cs b/ParadoxSaveParser.WebAPI/Game.cs new file mode 100644 index 0000000..20578b6 --- /dev/null +++ b/ParadoxSaveParser.WebAPI/Game.cs @@ -0,0 +1,7 @@ +namespace ParadoxSaveParser.WebAPI; + +public enum Game +{ + Unknown, + EU4 +} \ No newline at end of file diff --git a/ParadoxSaveParser.WebAPI/ParadoxSaveParser.WebAPI.csproj b/ParadoxSaveParser.WebAPI/ParadoxSaveParser.WebAPI.csproj index f47d841..8652737 100644 --- a/ParadoxSaveParser.WebAPI/ParadoxSaveParser.WebAPI.csproj +++ b/ParadoxSaveParser.WebAPI/ParadoxSaveParser.WebAPI.csproj @@ -15,6 +15,7 @@ + diff --git a/ParadoxSaveParser.WebAPI/PathHelper.cs b/ParadoxSaveParser.WebAPI/PathHelper.cs index ee55f52..76ffee0 100644 --- a/ParadoxSaveParser.WebAPI/PathHelper.cs +++ b/ParadoxSaveParser.WebAPI/PathHelper.cs @@ -6,16 +6,31 @@ public static class PathHelper { public static readonly IOPath DATA_DIR = "data"; public static readonly IOPath SAVES_DIR = Path.Concat(DATA_DIR, "saves"); + public static readonly IOPath PARSED_DIR = Path.Concat(DATA_DIR, "parsed"); + public static readonly IOPath TEMP_DIR = "temp"; - public static IOPath GetMetaFilePath(string save_id) => Path.Concat(SAVES_DIR, save_id + ".meta.json"); - - 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); + Directory.Create(PARSED_DIR); + Directory.Create(TEMP_DIR); + } + + public static void CleanTempDirectory() + { + Directory.Delete(TEMP_DIR); + Directory.Create(TEMP_DIR); + } + + public static Stream CreateTempFile() + { + string fileName = Guid.NewGuid().ToString(); + IOPath filePath = Path.Concat(TEMP_DIR, fileName); + var stream = System.IO.File.Open(filePath.Str, FileMode.CreateNew, FileAccess.ReadWrite, + FileShare.Read | FileShare.Delete); + // file stays as a ghost until it is closed + File.Delete(filePath); + return stream; } } \ No newline at end of file diff --git a/ParadoxSaveParser.WebAPI/Program.cs b/ParadoxSaveParser.WebAPI/Program.cs index 918d94d..a6df210 100644 --- a/ParadoxSaveParser.WebAPI/Program.cs +++ b/ParadoxSaveParser.WebAPI/Program.cs @@ -11,28 +11,25 @@ global using ParadoxSaveParser.Lib; global using Directory = DTLib.Filesystem.Directory; global using File = DTLib.Filesystem.File; global using Path = DTLib.Filesystem.Path; -using System.Collections.Concurrent; -using System.IO; using DTLib.Console; using DTLib.Dtsod; using DTLib.Web; using DTLib.Web.Routes; using ParadoxSaveParser.WebAPI.BackgroundTasks; +using ParadoxSaveParser.WebAPI.Database; using ParadoxSaveParser.WebAPI.Routes; using ParadoxSaveParser.WebAPI.SaveDataFilters; +// ReSharper disable MethodHasAsyncOverload namespace ParadoxSaveParser.WebAPI; public static class Program { - private static readonly IOPath _configPath = "./config.dtsod"; - private static Config _config = new(); - private static bool IsDebug = true; - private static readonly CancellationTokenSource _mainCancel = new(); - internal static readonly ConcurrentDictionary _saveMetadataStorage = new(); + internal static bool IsDebug = true; + internal static DatabaseConnector DB = null!; - public static void Main(string[] args) + public static async Task Main(string[] args) { Console.InputEncoding = Encoding.UTF8; Console.OutputEncoding = Encoding.UTF8; @@ -59,45 +56,37 @@ public static class Program }); var loggerMain = new ContextLogger(nameof(Main), loggerRoot); loggerMain.LogDebug("Debug log is enabled"); - + + CancellationTokenSource mainCancel = new(); Console.CancelKeyPress += (_, e) => { e.Cancel = true; loggerMain.LogInfo("Ctrl+C Pressed"); - _mainCancel.Cancel(); + mainCancel.Cancel(); }; try { // config - if (!File.Exists(_configPath)) + IOPath configPath = "./config.dtsod"; + Config config; + if (File.Exists(configPath)) { - loggerMain.LogWarn("config file not found."); - File.WriteAllText(_configPath, _config.ToString()); - loggerMain.LogWarn($"created default at {_configPath}."); + config = Config.FromDtsod(new DtsodV23(File.ReadAllText(configPath))); } else { - _config = Config.FromDtsod(new DtsodV23(File.ReadAllText(_configPath))); + loggerMain.LogWarn("config file not found."); + config = new(); + File.WriteAllText(configPath, config.ToString()); + loggerMain.LogWarn($"created default at {configPath}."); } - + 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!"); - } + PathHelper.CleanTempDirectory(); + DB = new("database.sqlite", 30, loggerRoot); + await DB.InitializeAsync(); + await DB.DeleteInvalidData(); var bgJobManager = new BackgroundJobManager(loggerRoot); var saveFilters = new Dictionary @@ -108,13 +97,13 @@ public static class Program // http server var router = new SimpleRouter(loggerRoot); router.DefaultRoute = new ServeFilesRouteHandler("public"); - router.MapRoute("/getSaveStatus", HttpMethod.GET, new GetSaveStatusHandler(_mainCancel.Token)); - router.MapRoute("/uploadSave/eu4", HttpMethod.POST, new UploadSaveHandler(_mainCancel.Token, + router.MapRoute("/getSaveStatus", HttpMethod.GET, new GetSaveStatusHandler(mainCancel.Token)); + router.MapRoute("/uploadSave", HttpMethod.POST, new UploadSaveHandler(mainCancel.Token, bgJobManager, saveFilters)); - router.MapRoute("/getSaveData", HttpMethod.GET, new GetSaveDataHandler(_mainCancel.Token)); + router.MapRoute("/getSaveData", HttpMethod.GET, new GetSaveDataHandler(mainCancel.Token)); - var app = new WebApp(_config.BaseUrl, loggerRoot, router, _mainCancel.Token); - app.Run().GetAwaiter().GetResult(); + var app = new WebApp(config.BaseUrl, loggerRoot, router, mainCancel.Token); + await app.Run(); } catch (OperationCanceledException ex) { @@ -125,6 +114,4 @@ public static class Program loggerMain.LogError(ex.ToStringDemystified()); } } - - } \ No newline at end of file diff --git a/ParadoxSaveParser.WebAPI/README.md b/ParadoxSaveParser.WebAPI/README.md index 5daa402..f758b40 100644 --- a/ParadoxSaveParser.WebAPI/README.md +++ b/ParadoxSaveParser.WebAPI/README.md @@ -1,10 +1,15 @@ # WebAPI Simple web application created using DTLib.Web. -# Routes -### POST `/uploadSave/eu4` -- **Request:** `application/octet-stream` - .eu4 file +## Important +Restart web application once per day to delete outdated data and clean RAM. + +## Routes +### POST `/uploadSave` +- **Query Params:** + - `game` - short name of the game (see [../README.md](../README.md)) +- **Request Body:** `application/octet-stream` - .eu4 file - **Response:** ```json { "saveId": "string" } diff --git a/ParadoxSaveParser.WebAPI/Routes/GetSaveDataHandler.cs b/ParadoxSaveParser.WebAPI/Routes/GetSaveDataHandler.cs index 1aed08e..1b2dd4d 100644 --- a/ParadoxSaveParser.WebAPI/Routes/GetSaveDataHandler.cs +++ b/ParadoxSaveParser.WebAPI/Routes/GetSaveDataHandler.cs @@ -1,4 +1,5 @@ using System.Net; +using ParadoxSaveParser.WebAPI.Database; using ParadoxSaveParser.WebAPI.HttpHelpers; namespace ParadoxSaveParser.WebAPI.Routes; @@ -18,7 +19,8 @@ internal class GetSaveDataHandler : RouteHandlerBase return await ReturnHelper.ResponseError(ctx, requestLogger, _cancelAllToken, idOrError.Error!); string id = idOrError.Value!; - if(!Program._saveMetadataStorage.TryGetValue(id, out var meta)) + var meta = await Program.DB.GetMetadata(id); + if(meta is null) return await ReturnHelper.ResponseError(ctx, requestLogger, _cancelAllToken, new ErrorMessage(HttpStatusCode.NotFound, $"Save with id {id} not found")); @@ -27,7 +29,7 @@ internal class GetSaveDataHandler : RouteHandlerBase new ErrorMessage(HttpStatusCode.BadRequest, $"Save with id {id} has status {meta.status}")); - IOPath dataFilePath = PathHelper.GetParsedSaveFilePath(id); + IOPath dataFilePath = meta.GetParsedDataPath(); if (!File.Exists(dataFilePath)) return await ReturnHelper.ResponseError(ctx, requestLogger, _cancelAllToken, new ErrorMessage(HttpStatusCode.InternalServerError, diff --git a/ParadoxSaveParser.WebAPI/Routes/GetSaveStatusHandler.cs b/ParadoxSaveParser.WebAPI/Routes/GetSaveStatusHandler.cs index bef7e63..9b88a62 100644 --- a/ParadoxSaveParser.WebAPI/Routes/GetSaveStatusHandler.cs +++ b/ParadoxSaveParser.WebAPI/Routes/GetSaveStatusHandler.cs @@ -17,8 +17,9 @@ internal class GetSaveStatusHandler : RouteHandlerBase if (idOrError.HasError) return await ReturnHelper.ResponseError(ctx, requestLogger, _cancelAllToken, idOrError.Error!); string id = idOrError.Value!; - - if (!Program._saveMetadataStorage.TryGetValue(id, out var meta)) + + var meta = await Program.DB.GetMetadata(id); + if (meta is null) return await ReturnHelper.ResponseError(ctx, requestLogger, _cancelAllToken, new ErrorMessage(HttpStatusCode.InternalServerError, $"Save with id {id} not found") diff --git a/ParadoxSaveParser.WebAPI/Routes/UploadSaveHandler.cs b/ParadoxSaveParser.WebAPI/Routes/UploadSaveHandler.cs index 6d3b960..cfae367 100644 --- a/ParadoxSaveParser.WebAPI/Routes/UploadSaveHandler.cs +++ b/ParadoxSaveParser.WebAPI/Routes/UploadSaveHandler.cs @@ -1,6 +1,8 @@ using System.Linq; using System.Net; +using System.Reflection.Metadata; using ParadoxSaveParser.WebAPI.BackgroundTasks; +using ParadoxSaveParser.WebAPI.Database; using ParadoxSaveParser.WebAPI.HttpHelpers; using ParadoxSaveParser.WebAPI.SaveDataFilters; @@ -28,34 +30,28 @@ public class UploadSaveHandler : RouteHandlerBase if (contentType != "application/octet-stream") return await ReturnHelper.ResponseError(ctx, requestLogger, _cancelAllToken, new ErrorMessage(HttpStatusCode.BadRequest, - $"Invalid request Content-Type: '{contentType}'") - ); + $"Invalid request Content-Type: '{contentType}'")); - string saveId = Guid.NewGuid().ToString(); - var metaFilePath = PathHelper.GetMetaFilePath(saveId); - if (File.Exists(metaFilePath)) + var gameStrOrError = RequestHelper.GetQueryValue(ctx, "game"); + if (gameStrOrError.HasError) + return await ReturnHelper.ResponseError(ctx, requestLogger, _cancelAllToken, gameStrOrError.Error!); + + if (!Enum.TryParse(gameStrOrError.Value, ignoreCase: true, out Game game)) return await ReturnHelper.ResponseError(ctx, requestLogger, _cancelAllToken, - new ErrorMessage(HttpStatusCode.InternalServerError, - $"Guid collision! file' {metaFilePath}' already exists.") - ); + new ErrorMessage(HttpStatusCode.BadRequest, + $"Invalid requested game: '{gameStrOrError.Value}'")); + var meta = await Program.DB.CreateMetadata(game); - 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; + var saveFilePath = meta.GetSaveFilePath(); + await using (var saveFile = File.OpenWrite(saveFilePath)) + { + await using (var remoteStream = ctx.Request.InputStream) + await remoteStream.CopyToAsync(saveFile, _cancelAllToken); + } + await Program.DB.UpdateMetadataStatus(meta, SaveFileProcessingStatus.Uploaded); _bgJobManager.StartNewParsingOperation(meta, _saveFilters[meta.game], _cancelAllToken); - dynamic responseData = new { saveId }; + dynamic responseData = new { meta.id }; 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 deleted file mode 100644 index 6f24201..0000000 --- a/ParadoxSaveParser.WebAPI/SaveFileMetadata.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Text.Json.Serialization; - -namespace ParadoxSaveParser.WebAPI; - -public enum SaveFileProcessingStatus -{ - Initialized, - Uploading, - Uploaded, - Parsing, - SavingResults, - Done -} - -public enum Game -{ - Unknown, - EU4 -} - -public class SaveFileMetadata -{ - private static readonly JsonSerializerOptions _configSerializerOptions = new() - { - WriteIndented = true, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - }; - public required string id { get; init; } - - [JsonConverter(typeof(JsonStringEnumConverter))] - public required Game game { get; init; } - - [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() - { - using var metaFile = File.OpenWrite(PathHelper.GetMetaFilePath(id)); - JsonSerializer.Serialize(metaFile, this, _configSerializerOptions); - } -} \ No newline at end of file diff --git a/TODO.md b/TODO.md index 352c48c..e65a029 100644 --- a/TODO.md +++ b/TODO.md @@ -1,8 +1,3 @@ -## WebAPI: - Temp files management system - Database to store metadata (SQLite-Net Extensions) - ## WebAPI.SaveParsingOperation: - Add debug log Save parsed data in protobuf Re-parse if saved data was parsed with another query