From d52cc4b20658abd6c66d70aa07bd3865a238e442 Mon Sep 17 00:00:00 2001 From: Lani Aung Date: Wed, 27 Oct 2021 07:58:18 -0600 Subject: [PATCH] Thinking about shunting most of the file parsing into db services since it should be done once --- Controllers/HomeController.cs | 25 ++--- Controllers/PlayController.cs | 23 ++++ Entities/CelConfigDto.cs | 10 ++ Entities/CelDto.cs | 3 + Entities/ConfigurationDto.cs | 10 +- Entities/KisekaeDto.cs | 5 +- Entities/PaletteDto.cs | 3 + Models/ConfigurationModel.cs | 21 ++++ Models/DirectoryModel.cs | 26 ----- Models/PlaysetModel.cs | 1 + Services/ConfigurationReaderService.cs | 67 ++++++------ Services/DatabaseService.cs | 141 ++++++++++++++++++++----- Startup.cs | 2 +- Views/Home/Index.cshtml | 10 +- Views/Play/Index.cshtml | 36 +++++++ fxl.codes.kisekae.csproj | 1 + 16 files changed, 271 insertions(+), 113 deletions(-) create mode 100644 Controllers/PlayController.cs create mode 100644 Entities/CelConfigDto.cs create mode 100644 Models/ConfigurationModel.cs delete mode 100644 Models/DirectoryModel.cs create mode 100644 Views/Play/Index.cshtml diff --git a/Controllers/HomeController.cs b/Controllers/HomeController.cs index 5264bfa..84ad108 100644 --- a/Controllers/HomeController.cs +++ b/Controllers/HomeController.cs @@ -1,8 +1,8 @@ using System; -using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.IO.IsolatedStorage; +using System.Linq; using System.Threading.Tasks; using fxl.codes.kisekae.Models; using fxl.codes.kisekae.Services; @@ -29,33 +29,20 @@ namespace fxl.codes.kisekae.Controllers } [HttpGet] - public IActionResult Index() + public async Task Index() { - var directories = _storage.GetDirectoryNames(); - var model = new List(); - - foreach (var directory in directories) - { - var files = _storage.GetFileNames(Path.Combine(directory, "*.cnf")); - if (files.Length <= 0) continue; - - var doll = new DirectoryModel(directory); - foreach (var file in files) doll.Configurations.Add(new ConfigurationModel(file)); - - model.Add(doll); - } - - return View(model); + var files = await _databaseService.GetAll(); + return View(files.Select(x => new ConfigurationModel(x))); } [HttpPost] - public async Task Upload(IFormFile file) + public IActionResult Upload(IFormFile file) { _logger.LogTrace($"File uploaded: {file?.FileName}"); if (!(file?.FileName.EndsWith("lzh", StringComparison.InvariantCultureIgnoreCase) ?? false)) throw new Exception("Please select a *.lzh file"); - await _databaseService.StoreToDatabase(file); + _databaseService.StoreToDatabase(file); return Redirect("/"); } diff --git a/Controllers/PlayController.cs b/Controllers/PlayController.cs new file mode 100644 index 0000000..25b9b20 --- /dev/null +++ b/Controllers/PlayController.cs @@ -0,0 +1,23 @@ +using System.Linq; +using System.Threading.Tasks; +using fxl.codes.kisekae.Models; +using fxl.codes.kisekae.Services; +using Microsoft.AspNetCore.Mvc; + +namespace fxl.codes.kisekae.Controllers +{ + public class PlayController : Controller + { + private readonly DatabaseService _databaseService; + + public PlayController(DatabaseService databaseService) + { + _databaseService = databaseService; + } + + public async Task Index(int id, int configId) + { + return View(); + } + } +} \ No newline at end of file diff --git a/Entities/CelConfigDto.cs b/Entities/CelConfigDto.cs new file mode 100644 index 0000000..4e29779 --- /dev/null +++ b/Entities/CelConfigDto.cs @@ -0,0 +1,10 @@ +using Dapper.Contrib.Extensions; + +namespace fxl.codes.kisekae.Entities +{ + [Table("cel_config")] + public class CelConfigDto + { + + } +} \ No newline at end of file diff --git a/Entities/CelDto.cs b/Entities/CelDto.cs index 1616f38..fa9b951 100644 --- a/Entities/CelDto.cs +++ b/Entities/CelDto.cs @@ -1,5 +1,8 @@ +using Dapper.Contrib.Extensions; + namespace fxl.codes.kisekae.Entities { + [Table("cel")] public class CelDto : IKisekaeFile, IKisekaeParseable { public int Id { get; set; } diff --git a/Entities/ConfigurationDto.cs b/Entities/ConfigurationDto.cs index c091d71..8e74a87 100644 --- a/Entities/ConfigurationDto.cs +++ b/Entities/ConfigurationDto.cs @@ -1,10 +1,18 @@ +using System.Collections.Generic; +using Dapper.Contrib.Extensions; + namespace fxl.codes.kisekae.Entities { + [Table("configuration")] public class ConfigurationDto : IKisekaeFile { public int Id { get; set; } + public int KisekaeId { get; set; } public string Filename { get; set; } public string Data { get; set; } - public int KisekaeId { get; set; } + public int? Height { get; set; } + public int? Width { get; set; } + public int? BorderIndex { get; set; } + public IEnumerable Cels { get; set; } } } \ No newline at end of file diff --git a/Entities/KisekaeDto.cs b/Entities/KisekaeDto.cs index d9ff5f1..a439f10 100644 --- a/Entities/KisekaeDto.cs +++ b/Entities/KisekaeDto.cs @@ -1,14 +1,15 @@ using System.Collections.Generic; +using Dapper.Contrib.Extensions; namespace fxl.codes.kisekae.Entities { + [Table("kisekae")] public class KisekaeDto { public int Id { get; set; } public string Name { get; set; } public string Filename { get; set; } public string Checksum { get; set; } - - public IEnumerable Cels { get; set; } + public IEnumerable Configurations { get; set; } } } \ No newline at end of file diff --git a/Entities/PaletteDto.cs b/Entities/PaletteDto.cs index 02cbb8d..0aad485 100644 --- a/Entities/PaletteDto.cs +++ b/Entities/PaletteDto.cs @@ -1,5 +1,8 @@ +using Dapper.Contrib.Extensions; + namespace fxl.codes.kisekae.Entities { + [Table("palette")] public class PaletteDto : IKisekaeFile, IKisekaeParseable { public int Id { get; set; } diff --git a/Models/ConfigurationModel.cs b/Models/ConfigurationModel.cs new file mode 100644 index 0000000..538ff24 --- /dev/null +++ b/Models/ConfigurationModel.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using fxl.codes.kisekae.Entities; + +namespace fxl.codes.kisekae.Models +{ + public class ConfigurationModel + { + public ConfigurationModel(KisekaeDto dto) + { + Id = dto.Id; + Name = dto.Name; + + Configurations = new Dictionary(); + foreach (var config in dto.Configurations) Configurations.Add(config.Id, config.Filename); + } + + public int Id { get; } + public string Name { get; } + public IDictionary Configurations { get; } + } +} \ No newline at end of file diff --git a/Models/DirectoryModel.cs b/Models/DirectoryModel.cs deleted file mode 100644 index f7c68dc..0000000 --- a/Models/DirectoryModel.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Collections.Generic; - -namespace fxl.codes.kisekae.Models -{ - public class DirectoryModel - { - public string Name { get; } - - public DirectoryModel(string name) - { - Name = name; - } - - public List Configurations { get; } = new(); - } - - public class ConfigurationModel - { - public string Name { get; } - - public ConfigurationModel(string name) - { - Name = name; - } - } -} \ No newline at end of file diff --git a/Models/PlaysetModel.cs b/Models/PlaysetModel.cs index 7dd7e9c..ca16eab 100644 --- a/Models/PlaysetModel.cs +++ b/Models/PlaysetModel.cs @@ -10,6 +10,7 @@ namespace fxl.codes.kisekae.Models public string Name { get; set; } public int Height { get; set; } public int Width { get; set; } + [JsonIgnore] public int BorderColorIndex { get; set; } [JsonIgnore] public Color BorderColor { get; set; } = Color.Black; [JsonIgnore] public List Palettes { get; } = new(); public List Cels { get; } = new(); diff --git a/Services/ConfigurationReaderService.cs b/Services/ConfigurationReaderService.cs index 42553d3..91ce750 100644 --- a/Services/ConfigurationReaderService.cs +++ b/Services/ConfigurationReaderService.cs @@ -10,6 +10,7 @@ namespace fxl.codes.kisekae.Services { public class ConfigurationReaderService { + public const string ResolutionRegexPattern = @"\((?[0-9]*).(?[0-9]*)\)"; private readonly FileParserService _fileParser; private readonly ILogger _logger; @@ -26,36 +27,9 @@ namespace fxl.codes.kisekae.Services var borderColorIndex = 0; using var reader = new StreamReader(fileStream); - while (!reader.EndOfStream) - { - var line = reader.ReadLine(); - if (string.IsNullOrEmpty(line)) continue; - - switch (line.ToCharArray()[0]) - { - case '(': - var resolutionRegex = new Regex(@"\((?[0-9]*).(?[0-9]*)\)"); - var resolutionMatch = resolutionRegex.Match(line); - model.Width = int.Parse(resolutionMatch.Groups["Width"].Value); - model.Height = int.Parse(resolutionMatch.Groups["Height"].Value); - break; - case '[': - var borderValue = line.Replace("[", ""); - if (borderValue.Contains(';')) borderValue = borderValue.Split(';')[0].Trim(); - borderColorIndex = int.Parse(borderValue); - break; - case '%': - model.Palettes.Add(new PaletteModel(_logger, line)); - break; - case '#': - model.Cels.Add(new CelModel(_logger, line)); - break; - case '$': - case ' ': - initialPositions.Append(line.Replace("\\r\\n", "").Replace("\\n", "")); - break; - } - } + var data = reader.ReadToEnd(); + + SetPlaysetProperties(data, model, initialPositions); SetInitialPositions(model, initialPositions.ToString()); if (string.IsNullOrEmpty(directory)) return model; @@ -75,6 +49,39 @@ namespace fxl.codes.kisekae.Services return model; } + public void SetPlaysetProperties(string data, PlaysetModel model, StringBuilder initialPositions) + { + if (string.IsNullOrEmpty(data)) return; + + foreach (var line in data.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries)) + { + switch (line.ToCharArray()[0]) + { + case '(': + var resolutionRegex = new Regex(ResolutionRegexPattern); + var resolutionMatch = resolutionRegex.Match(line); + model.Width = int.Parse(resolutionMatch.Groups["Width"].Value); + model.Height = int.Parse(resolutionMatch.Groups["Height"].Value); + break; + case '[': + var borderValue = line[1..]; + if (borderValue.Contains(';')) borderValue = borderValue.Split(';')[0].Trim(); + model.BorderColorIndex = int.Parse(borderValue); + break; + case '%': + model.Palettes.Add(new PaletteModel(_logger, line)); + break; + case '#': + model.Cels.Add(new CelModel(_logger, line)); + break; + case '$': + case ' ': + initialPositions.Append(line.Replace("\\r\\n", "").Replace("\\n", "")); + break; + } + } + } + private static void SetInitialPositions(PlaysetModel model, string positions) { var sets = positions.Split('$', StringSplitOptions.RemoveEmptyEntries); diff --git a/Services/DatabaseService.cs b/Services/DatabaseService.cs index 0349e25..1dc5ba8 100644 --- a/Services/DatabaseService.cs +++ b/Services/DatabaseService.cs @@ -1,10 +1,15 @@ using System; using System.Collections.Generic; +using System.Data; using System.IO; using System.IO.IsolatedStorage; +using System.Linq; using System.Security.Cryptography; +using System.Text; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Dapper; +using Dapper.Contrib.Extensions; using fxl.codes.kisekae.Entities; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; @@ -15,47 +20,96 @@ namespace fxl.codes.kisekae.Services { public class DatabaseService { + private const string ResolutionRegexPattern = @"\((?[0-9]*).(?[0-9]*)\)"; + private readonly ConfigurationReaderService _configurationReaderService; + private readonly string _connectionString; private readonly FileParserService _fileParserService; private readonly ILogger _logger; private readonly IsolatedStorageFile _storage; - public DatabaseService(ILogger logger, IConfiguration configuration, FileParserService fileParserService) + public DatabaseService(ILogger logger, + IConfiguration configuration, + ConfigurationReaderService configurationReaderService, + FileParserService fileParserService) { _logger = logger; + _configurationReaderService = configurationReaderService; _fileParserService = fileParserService; _connectionString = configuration.GetConnectionString("kisekae"); _storage = IsolatedStorageFile.GetUserStoreForApplication(); } - public async Task StoreToDatabase(IFormFile file) + public async Task> GetAll() { await using var connection = new NpgsqlConnection(_connectionString); await connection.OpenAsync(); - var existing = connection.QuerySingleOrDefault("select * from kisekae where kisekae.filename = @Filename", new { Filename = file.FileName }); - if (existing != null) + using var multi = await connection.QueryMultipleAsync("select * from kisekae; select * from configuration"); + var files = multi.Read(); + var configs = multi.Read(); + var dictionary = configs.ToLookup(x => x.KisekaeId); + await connection.CloseAsync(); + + foreach (var kiss in files) kiss.Configurations = dictionary[kiss.Id]; + return files; + } + + public async void GetKisekaeConfig(int id, int configId) + { + await using var connection = new NpgsqlConnection(_connectionString); + await connection.OpenAsync(); + + var queryParams = new { Id = id, ConfigId = configId }; + var cels = connection.Query("select * from cel_config where configId = @ConfigId", queryParams); + if (cels == null) { - _logger.LogInformation($"Already existing upload {file.FileName} under id {existing.Id}"); - return existing; + var config = connection.QuerySingle( + "select * from configuration where kisekae_id = @Id and id = @ConfigId", + queryParams); + + foreach (var line in config.Data.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries)) + switch (line.ToCharArray()[0]) + { + case '(': + var resolutionRegex = new Regex(ResolutionRegexPattern); + var resolutionMatch = resolutionRegex.Match(line); + config.Width = int.Parse(resolutionMatch.Groups["Width"].Value); + config.Height = int.Parse(resolutionMatch.Groups["Height"].Value); + break; + case '[': + var borderValue = line[1..]; + if (borderValue.Contains(';')) borderValue = borderValue.Split(';')[0].Trim(); + config.BorderIndex = int.Parse(borderValue); + break; + case '%': + break; + case '#': + break; + case '$': + case ' ': + break; + } + + await connection.UpdateAsync(config); } - await using var memoryStream = new MemoryStream(); - await file.CopyToAsync(memoryStream); + await connection.CloseAsync(); + } + public async void StoreToDatabase(IFormFile file) + { + await using var connection = new NpgsqlConnection(_connectionString); + await connection.OpenAsync(); + var memoryStream = await GetAsMemoryStream(file); var checksum = Convert.ToBase64String(await new SHA256Managed().ComputeHashAsync(memoryStream)); - existing = connection.QuerySingleOrDefault("select * from kisekae where kisekae.checksum = @Checksum", new { Checksum = checksum }); - if (existing != null) - { - _logger.LogInformation($"Already existing upload (checksum: {checksum}) under id {existing.Id} and filename {existing.Filename}"); - return existing; - } + memoryStream.Position = 0; // Reset for re-read - memoryStream.Position = 0; + var existing = GetExisting(connection, file.FileName, checksum); + if (existing != null) return; _fileParserService.UnzipLzh(file, memoryStream); var directory = Path.GetFileNameWithoutExtension(file.FileName); - var kisekae = new KisekaeDto { Filename = file.FileName, @@ -63,13 +117,33 @@ namespace fxl.codes.kisekae.Services Checksum = checksum }; - var id = connection.QueryFirst("insert into kisekae(name, filename, checksum) values (@Name, @Filename, @Checksum) returning id", kisekae); - kisekae.Id = id; + kisekae.Id = await connection.InsertAsync(kisekae); var filenames = _storage.GetFileNames($"{Path.Combine(directory, "*")}"); - var cels = new List(); - var palettes = new List(); - var configurations = new List(); + SetInnerFiles(directory, filenames, kisekae.Id); + + await connection.CloseAsync(); + } + + private KisekaeDto GetExisting(IDbConnection connection, string filename, string checksum) + { + var existing = connection.QuerySingleOrDefault("select * from kisekae where kisekae.filename = @Filename", new { Filename = filename }); + if (existing != null) + { + _logger.LogInformation($"Already existing upload {filename} under id {existing.Id}"); + return existing; + } + + existing = connection.QuerySingleOrDefault("select * from kisekae where kisekae.checksum = @Checksum", new { Checksum = checksum }); + if (existing == null) return null; + + _logger.LogInformation($"Already existing upload (checksum: {checksum}) under id {existing.Id} and filename {existing.Filename}"); + return existing; + } + + private async void SetInnerFiles(string directory, IEnumerable filenames, int id) + { + var files = new List(); foreach (var filename in filenames) { await using var reader = _storage.OpenFile(Path.Combine(directory, filename), FileMode.Open); @@ -81,7 +155,7 @@ namespace fxl.codes.kisekae.Services switch (Path.GetExtension(filename).ToLower()) { case ".cel": - cels.Add(new CelDto + files.Add(new CelDto { Filename = filename, Data = bytes, @@ -89,15 +163,15 @@ namespace fxl.codes.kisekae.Services }); break; case ".cnf": - configurations.Add(new ConfigurationDto + files.Add(new ConfigurationDto { Filename = filename, - Data = BitConverter.ToString(bytes), + Data = Encoding.ASCII.GetString(bytes), KisekaeId = id }); break; case ".kcf": - palettes.Add(new PaletteDto + files.Add(new PaletteDto { Filename = filename, Data = bytes, @@ -107,13 +181,22 @@ namespace fxl.codes.kisekae.Services } } - await connection.ExecuteAsync("insert into cel (filename, data, kisekae_id) values (@Filename, @Data, @KisekaeId)", cels); - await connection.ExecuteAsync("insert into configuration (filename, data, kisekae_id) values (@Filename, @Data, @KisekaeId)", configurations); - await connection.ExecuteAsync("insert into palette (filename, data, kisekae_id) values (@Filename, @Data, @KisekaeId)", palettes); + await using var connection = new NpgsqlConnection(_connectionString); + await connection.OpenAsync(); + + await connection.InsertAsync(files.OfType()); + await connection.InsertAsync(files.OfType()); + await connection.InsertAsync(files.OfType()); await connection.CloseAsync(); + } - return kisekae; + private static async Task GetAsMemoryStream(IFormFile file) + { + var stream = new MemoryStream(); + await file.CopyToAsync(stream); + + return stream; } } } \ No newline at end of file diff --git a/Startup.cs b/Startup.cs index 6422aa4..ac94ee1 100644 --- a/Startup.cs +++ b/Startup.cs @@ -56,7 +56,7 @@ namespace fxl.codes.kisekae { endpoints.MapControllerRoute( "default", - "{controller=Home}/{action=Index}/{id?}"); + "{controller=Home}/{action=Index}/{id?}/{configId?}"); }); DefaultTypeMap.MatchNamesWithUnderscores = true; diff --git a/Views/Home/Index.cshtml b/Views/Home/Index.cshtml index 7360216..6f2b98a 100644 --- a/Views/Home/Index.cshtml +++ b/Views/Home/Index.cshtml @@ -1,4 +1,4 @@ -@model IEnumerable +@model IEnumerable @{ ViewData["Title"] = "Select"; @@ -10,15 +10,15 @@
    - @foreach (var directory in Model) + @foreach (var kiss in Model) {
  • - @directory.Name + @kiss.Name
      - @foreach (var config in directory.Configurations) + @foreach (var (key, value) in kiss.Configurations) {
    • - @config.Name + @value
    • }
    diff --git a/Views/Play/Index.cshtml b/Views/Play/Index.cshtml new file mode 100644 index 0000000..ba9eef0 --- /dev/null +++ b/Views/Play/Index.cshtml @@ -0,0 +1,36 @@ +@model PlaysetModel + +@{ + ViewData["Title"] = $"Play with {Model.Name}"; +} + +
    +
      +
    • + +
    • +
    • Sets
    • +
    +
    +
    +
    + @for (var index = 0; index < Model.Cels.Count; index++) + { + var cel = Model.Cels[index]; +
    + } +
    +
    +@section Scripts +{ + +} \ No newline at end of file diff --git a/fxl.codes.kisekae.csproj b/fxl.codes.kisekae.csproj index 05c8fb4..5e79b72 100644 --- a/fxl.codes.kisekae.csproj +++ b/fxl.codes.kisekae.csproj @@ -14,6 +14,7 @@ +