From b1f32e6f13c9d9cd5040f108b26a257123bf8179 Mon Sep 17 00:00:00 2001 From: Natalia Portillo Date: Thu, 13 Nov 2025 05:16:58 +0000 Subject: [PATCH] Moved startup code to server. --- .../Controllers/WeatherForecastController.cs | 3 + Marechai.Server/Helpers/Photos.cs | 587 ++++++++++++++++++ Marechai.Server/Helpers/SvgRender.cs | 288 +++++++++ Marechai.Server/Interop/DetectOS.cs | 2 +- Marechai.Server/Interop/PlatformID.cs | 2 +- Marechai.Server/Interop/Version.cs | 7 +- Marechai.Server/Marechai.Server.csproj | 19 +- Marechai.Server/Program.cs | 216 ++++++- Marechai.Server/WeatherForecast.cs | 2 + Marechai/Program.cs | 201 +----- 10 files changed, 1106 insertions(+), 221 deletions(-) create mode 100644 Marechai.Server/Helpers/Photos.cs create mode 100644 Marechai.Server/Helpers/SvgRender.cs diff --git a/Marechai.Server/Controllers/WeatherForecastController.cs b/Marechai.Server/Controllers/WeatherForecastController.cs index a307510a..fd6b209a 100644 --- a/Marechai.Server/Controllers/WeatherForecastController.cs +++ b/Marechai.Server/Controllers/WeatherForecastController.cs @@ -1,3 +1,6 @@ +using System; +using System.Collections.Generic; +using System.Linq; using Microsoft.AspNetCore.Mvc; namespace Marechai.Server.Controllers; diff --git a/Marechai.Server/Helpers/Photos.cs b/Marechai.Server/Helpers/Photos.cs new file mode 100644 index 00000000..de84b92c --- /dev/null +++ b/Marechai.Server/Helpers/Photos.cs @@ -0,0 +1,587 @@ +/****************************************************************************** +// MARECHAI: Master repository of computing history artifacts information +// ---------------------------------------------------------------------------- +// +// Author(s) : Natalia Portillo +// +// --[ License ] -------------------------------------------------------------- +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// ---------------------------------------------------------------------------- +// Copyright © 2003-2021 Natalia Portillo +*******************************************************************************/ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +namespace Marechai.Helpers; + +public class Photos +{ + public delegate Task ConversionFinished(bool result); + + public static void EnsureCreated(string webRootPath, bool scan, string item) + { + List paths = []; + + string photosRoot = Path.Combine(webRootPath, "assets", scan ? "scan" : "photos"); + string itemPhotosRoot = Path.Combine(photosRoot, item); + string itemThumbsRoot = Path.Combine(itemPhotosRoot, "thumbs"); + string itemOriginalPhotosRoot = Path.Combine(itemPhotosRoot, "originals"); + + paths.Add(photosRoot); + paths.Add(itemPhotosRoot); + paths.Add(itemThumbsRoot); + paths.Add(itemOriginalPhotosRoot); + + paths.Add(Path.Combine(itemThumbsRoot, "jpeg", "hd")); + paths.Add(Path.Combine(itemThumbsRoot, "jpeg", "1440p")); + paths.Add(Path.Combine(itemThumbsRoot, "jpeg", "4k")); + paths.Add(Path.Combine(itemPhotosRoot, "jpeg", "hd")); + paths.Add(Path.Combine(itemPhotosRoot, "jpeg", "1440p")); + paths.Add(Path.Combine(itemPhotosRoot, "jpeg", "4k")); + + paths.Add(Path.Combine(itemThumbsRoot, "jp2k", "hd")); + paths.Add(Path.Combine(itemThumbsRoot, "jp2k", "1440p")); + paths.Add(Path.Combine(itemThumbsRoot, "jp2k", "4k")); + paths.Add(Path.Combine(itemPhotosRoot, "jp2k", "hd")); + paths.Add(Path.Combine(itemPhotosRoot, "jp2k", "1440p")); + paths.Add(Path.Combine(itemPhotosRoot, "jp2k", "4k")); + + paths.Add(Path.Combine(itemThumbsRoot, "webp", "hd")); + paths.Add(Path.Combine(itemThumbsRoot, "webp", "1440p")); + paths.Add(Path.Combine(itemThumbsRoot, "webp", "4k")); + paths.Add(Path.Combine(itemPhotosRoot, "webp", "hd")); + paths.Add(Path.Combine(itemPhotosRoot, "webp", "1440p")); + paths.Add(Path.Combine(itemPhotosRoot, "webp", "4k")); + + paths.Add(Path.Combine(itemThumbsRoot, "heif", "hd")); + paths.Add(Path.Combine(itemThumbsRoot, "heif", "1440p")); + paths.Add(Path.Combine(itemThumbsRoot, "heif", "4k")); + paths.Add(Path.Combine(itemPhotosRoot, "heif", "hd")); + paths.Add(Path.Combine(itemPhotosRoot, "heif", "1440p")); + paths.Add(Path.Combine(itemPhotosRoot, "heif", "4k")); + + paths.Add(Path.Combine(itemThumbsRoot, "avif", "hd")); + paths.Add(Path.Combine(itemThumbsRoot, "avif", "1440p")); + paths.Add(Path.Combine(itemThumbsRoot, "avif", "4k")); + paths.Add(Path.Combine(itemPhotosRoot, "avif", "hd")); + paths.Add(Path.Combine(itemPhotosRoot, "avif", "1440p")); + paths.Add(Path.Combine(itemPhotosRoot, "avif", "4k")); + + foreach(string path in paths.Where(path => !Directory.Exists(path))) Directory.CreateDirectory(path); + } + + public static bool Convert(string webRootPath, Guid id, string originalPath, string sourceFormat, + string outputFormat, string resolution, bool thumbnail, bool scan, string item) + { + outputFormat = outputFormat.ToLowerInvariant(); + resolution = resolution.ToLowerInvariant(); + sourceFormat = sourceFormat.ToLowerInvariant(); + + string outputPath = Path.Combine(webRootPath, "assets", scan ? "scans" : "photos", item); + int width, height; + + if(thumbnail) outputPath = Path.Combine(outputPath, "thumbs"); + + outputPath = Path.Combine(outputPath, outputFormat); + outputPath = Path.Combine(outputPath, resolution); + + switch(resolution) + { + case "hd": + if(thumbnail) + { + width = 256; + height = 256; + } + else + { + width = 1920; + height = 1080; + } + + break; + case "1440p": + if(thumbnail) + { + width = 384; + height = 384; + } + else + { + width = 2560; + height = 1440; + } + + break; + case "4k": + if(thumbnail) + { + width = 512; + height = 512; + } + else + { + width = 3840; + height = 2160; + } + + break; + default: + return false; + } + + string tmpPath; + bool ret; + + switch(outputFormat) + { + case "jpeg": + outputPath = Path.Combine(outputPath, $"{id}.jpg"); + + return ConvertUsingImageMagick(originalPath, outputPath, width, height); + case "jp2k": + outputPath = Path.Combine(outputPath, $"{id}.jp2"); + + return ConvertUsingImageMagick(originalPath, outputPath, width, height); + + case "webp": + outputPath = Path.Combine(outputPath, $"{id}.webp"); + + return ConvertUsingImageMagick(originalPath, outputPath, width, height); + + case "heif": + outputPath = Path.Combine(outputPath, $"{id}.heic"); + + return ConvertUsingImageMagick(originalPath, outputPath, width, height); + + case "avif": + outputPath = Path.Combine(outputPath, $"{id}.avif"); + + tmpPath = Path.GetTempFileName(); + File.Delete(tmpPath); + tmpPath += ".png"; + + // AVIFENC does not resize + ret = ConvertUsingImageMagick(originalPath, tmpPath, width, height); + + if(!ret) + { + File.Delete(tmpPath); + + return ret; + } + + ret = ConvertToAvif(tmpPath, outputPath, width, height); + + File.Delete(tmpPath); + + return ret; + default: + return false; + } + } + + public static bool ConvertUsingImageMagick(string originalPath, string outputPath, int width, int height) + { + var convert = new Process + { + StartInfo = + { + FileName = "convert", + CreateNoWindow = true, + RedirectStandardError = true, + RedirectStandardOutput = true, + ArgumentList = + { + "-resize", + $"{width}x{height}", + "-strip", + originalPath, + outputPath + } + } + }; + + try + { + convert.Start(); + convert.StandardOutput.ReadToEnd(); + convert.WaitForExit(); + + return convert.ExitCode == 0; + } + catch(Exception) + { + return false; + } + } + + public static bool ConvertToAvif(string originalPath, string outputPath, int width, int height) + { + var avif = new Process + { + StartInfo = + { + FileName = "avifenc", + CreateNoWindow = true, + RedirectStandardError = true, + RedirectStandardOutput = true, + ArgumentList = + { + "-j", + "4", + originalPath, + outputPath + } + } + }; + + try + { + avif.Start(); + avif.StandardOutput.ReadToEnd(); + avif.WaitForExit(); + + return avif.ExitCode == 0; + } + catch(Exception) + { + return false; + } + } + + public void ConversionWorker(string webRootPath, Guid id, string originalFilePath, string sourceFormat, bool scan, + string item) + { + List pool = + [ + new(() => FinishedRenderingJpeg4kThumbnail?.Invoke(Convert(webRootPath, + id, + originalFilePath, + sourceFormat, + "JPEG", + "4k", + true, + scan, + item))), + new(() => FinishedRenderingJpeg1440Thumbnail?.Invoke(Convert(webRootPath, + id, + originalFilePath, + sourceFormat, + "JPEG", + "1440p", + true, + scan, + item))), + new(() => FinishedRenderingJpegHdThumbnail?.Invoke(Convert(webRootPath, + id, + originalFilePath, + sourceFormat, + "JPEG", + "hd", + true, + scan, + item))), + new(() => FinishedRenderingJpeg4K?.Invoke(Convert(webRootPath, + id, + originalFilePath, + sourceFormat, + "JPEG", + "4k", + false, + scan, + item))), + new(() => FinishedRenderingJpeg1440?.Invoke(Convert(webRootPath, + id, + originalFilePath, + sourceFormat, + "JPEG", + "1440p", + false, + scan, + item))), + new(() => FinishedRenderingJpegHd?.Invoke(Convert(webRootPath, + id, + originalFilePath, + sourceFormat, + "JPEG", + "hd", + false, + scan, + item))), + new(() => FinishedRenderingJp2k4kThumbnail?.Invoke(Convert(webRootPath, + id, + originalFilePath, + sourceFormat, + "JP2K", + "4k", + true, + scan, + item))), + new(() => FinishedRenderingJp2k1440Thumbnail?.Invoke(Convert(webRootPath, + id, + originalFilePath, + sourceFormat, + "JP2K", + "1440p", + true, + scan, + item))), + new(() => FinishedRenderingJp2kHdThumbnail?.Invoke(Convert(webRootPath, + id, + originalFilePath, + sourceFormat, + "JP2K", + "hd", + true, + scan, + item))), + new(() => FinishedRenderingJp2k4k?.Invoke(Convert(webRootPath, + id, + originalFilePath, + sourceFormat, + "JP2K", + "4k", + false, + scan, + item))), + new(() => FinishedRenderingJp2k1440?.Invoke(Convert(webRootPath, + id, + originalFilePath, + sourceFormat, + "JP2K", + "1440p", + false, + scan, + item))), + new(() => FinishedRenderingJp2kHd?.Invoke(Convert(webRootPath, + id, + originalFilePath, + sourceFormat, + "JP2K", + "hd", + false, + scan, + item))), + new(() => FinishedRenderingWebp4kThumbnail?.Invoke(Convert(webRootPath, + id, + originalFilePath, + sourceFormat, + "WEBP", + "4k", + true, + scan, + item))), + new(() => FinishedRenderingWebp1440Thumbnail?.Invoke(Convert(webRootPath, + id, + originalFilePath, + sourceFormat, + "WEBP", + "1440p", + true, + scan, + item))), + new(() => FinishedRenderingWebpHdThumbnail?.Invoke(Convert(webRootPath, + id, + originalFilePath, + sourceFormat, + "WEBP", + "hd", + true, + scan, + item))), + new(() => FinishedRenderingWebp4k?.Invoke(Convert(webRootPath, + id, + originalFilePath, + sourceFormat, + "WEBP", + "4k", + false, + scan, + item))), + new(() => FinishedRenderingWebp1440?.Invoke(Convert(webRootPath, + id, + originalFilePath, + sourceFormat, + "WEBP", + "1440p", + false, + scan, + item))), + new(() => FinishedRenderingWebpHd?.Invoke(Convert(webRootPath, + id, + originalFilePath, + sourceFormat, + "WEBP", + "hd", + false, + scan, + item))), + new(() => FinishedRenderingHeif4kThumbnail?.Invoke(Convert(webRootPath, + id, + originalFilePath, + sourceFormat, + "HEIF", + "4k", + true, + scan, + item))), + new(() => FinishedRenderingHeif1440Thumbnail?.Invoke(Convert(webRootPath, + id, + originalFilePath, + sourceFormat, + "HEIF", + "1440p", + true, + scan, + item))), + new(() => FinishedRenderingHeifHdThumbnail?.Invoke(Convert(webRootPath, + id, + originalFilePath, + sourceFormat, + "HEIF", + "hd", + true, + scan, + item))), + new(() => FinishedRenderingHeif4K?.Invoke(Convert(webRootPath, + id, + originalFilePath, + sourceFormat, + "HEIF", + "4k", + false, + scan, + item))), + new(() => FinishedRenderingHeif1440?.Invoke(Convert(webRootPath, + id, + originalFilePath, + sourceFormat, + "HEIF", + "1440p", + false, + scan, + item))), + new(() => FinishedRenderingHeifHd?.Invoke(Convert(webRootPath, + id, + originalFilePath, + sourceFormat, + "HEIF", + "hd", + false, + scan, + item))), + new(() => FinishedRenderingAvif4kThumbnail?.Invoke(Convert(webRootPath, + id, + originalFilePath, + sourceFormat, + "AVIF", + "4k", + true, + scan, + item))), + new(() => FinishedRenderingAvif1440Thumbnail?.Invoke(Convert(webRootPath, + id, + originalFilePath, + sourceFormat, + "AVIF", + "1440p", + true, + scan, + item))), + new(() => FinishedRenderingAvifHdThumbnail?.Invoke(Convert(webRootPath, + id, + originalFilePath, + sourceFormat, + "AVIF", + "hd", + true, + scan, + item))), + new(() => FinishedRenderingAvif4K?.Invoke(Convert(webRootPath, + id, + originalFilePath, + sourceFormat, + "AVIF", + "4k", + false, + scan, + item))), + new(() => FinishedRenderingAvif1440?.Invoke(Convert(webRootPath, + id, + originalFilePath, + sourceFormat, + "AVIF", + "1440p", + false, + scan, + item))), + new(() => FinishedRenderingAvifHd?.Invoke(Convert(webRootPath, + id, + originalFilePath, + sourceFormat, + "AVIF", + "hd", + false, + scan, + item))) + ]; + + foreach(Task thread in pool) thread.Start(); + + Task.WaitAll(pool.ToArray()); + + FinishedAll?.Invoke(true); + } + + public event ConversionFinished FinishedAll; + + public event ConversionFinished FinishedRenderingJpegHdThumbnail; + public event ConversionFinished FinishedRenderingJpeg1440Thumbnail; + public event ConversionFinished FinishedRenderingJpeg4kThumbnail; + public event ConversionFinished FinishedRenderingJpegHd; + public event ConversionFinished FinishedRenderingJpeg1440; + public event ConversionFinished FinishedRenderingJpeg4K; + public event ConversionFinished FinishedRenderingJp2kHdThumbnail; + public event ConversionFinished FinishedRenderingJp2k1440Thumbnail; + public event ConversionFinished FinishedRenderingJp2k4kThumbnail; + public event ConversionFinished FinishedRenderingJp2kHd; + public event ConversionFinished FinishedRenderingJp2k1440; + public event ConversionFinished FinishedRenderingJp2k4k; + public event ConversionFinished FinishedRenderingWebpHdThumbnail; + public event ConversionFinished FinishedRenderingWebp1440Thumbnail; + public event ConversionFinished FinishedRenderingWebp4kThumbnail; + public event ConversionFinished FinishedRenderingWebpHd; + public event ConversionFinished FinishedRenderingWebp1440; + public event ConversionFinished FinishedRenderingWebp4k; + public event ConversionFinished FinishedRenderingHeifHdThumbnail; + public event ConversionFinished FinishedRenderingHeif1440Thumbnail; + public event ConversionFinished FinishedRenderingHeif4kThumbnail; + public event ConversionFinished FinishedRenderingHeifHd; + public event ConversionFinished FinishedRenderingHeif1440; + public event ConversionFinished FinishedRenderingHeif4K; + public event ConversionFinished FinishedRenderingAvifHdThumbnail; + public event ConversionFinished FinishedRenderingAvif1440Thumbnail; + public event ConversionFinished FinishedRenderingAvif4kThumbnail; + public event ConversionFinished FinishedRenderingAvifHd; + public event ConversionFinished FinishedRenderingAvif1440; + public event ConversionFinished FinishedRenderingAvif4K; +} \ No newline at end of file diff --git a/Marechai.Server/Helpers/SvgRender.cs b/Marechai.Server/Helpers/SvgRender.cs new file mode 100644 index 00000000..d6ac3d5f --- /dev/null +++ b/Marechai.Server/Helpers/SvgRender.cs @@ -0,0 +1,288 @@ +/****************************************************************************** +// MARECHAI: Master repository of computing history artifacts information +// ---------------------------------------------------------------------------- +// +// Author(s) : Natalia Portillo +// +// --[ License ] -------------------------------------------------------------- +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// ---------------------------------------------------------------------------- +// Copyright © 2003-2021 Natalia Portillo +*******************************************************************************/ + +using System; +using System.IO; +using Marechai.Database.Models; +using SkiaSharp; +using Svg.Skia; + +namespace Marechai.Server.Helpers; + +public static class SvgRender +{ + public static void RenderCountries() + { + if(!Directory.Exists("wwwroot/assets/flags/countries")) return; + + foreach(string file in Directory.GetFiles("wwwroot/assets/flags/countries/", + "*.svg", + SearchOption.TopDirectoryOnly)) + { + SKSvg svg = null; + + string flagName = Path.GetFileNameWithoutExtension(file); + + foreach(string format in new[] + { + "png", "webp" + }) + { + if(!Directory.Exists(Path.Combine("wwwroot/assets/flags/countries", format))) + Directory.CreateDirectory(Path.Combine("wwwroot/assets/flags/countries", format)); + + SKEncodedImageFormat skFormat; + + switch(format) + { + case "webp": + skFormat = SKEncodedImageFormat.Webp; + + break; + default: + skFormat = SKEncodedImageFormat.Png; + + break; + } + + foreach(int multiplier in new[] + { + 1, 2, 3 + }) + { + if(!Directory.Exists(Path.Combine("wwwroot/assets/flags/countries", format, $"{multiplier}x"))) + { + Directory.CreateDirectory(Path.Combine("wwwroot/assets/flags/countries", + format, + $"{multiplier}x")); + } + + string rendered = Path.Combine("wwwroot/assets/flags/countries", + format, + $"{multiplier}x", + flagName + $".{format}"); + + if(File.Exists(rendered)) continue; + + Console.WriteLine("Rendering {0}", rendered); + + if(svg == null) + { + svg = new SKSvg(); + svg.Load(file); + } + + var outFs = new FileStream(rendered, FileMode.CreateNew); + RenderSvg(svg, outFs, skFormat, 32, multiplier); + outFs.Close(); + } + } + } + } + + public static void ImportCompanyLogos(MarechaiContext context) + { + if(!Directory.Exists("wwwroot/assets/incoming")) return; + + foreach(string file in Directory.GetFiles("wwwroot/assets/incoming", + "company_*.svg", + SearchOption.TopDirectoryOnly)) + { + string filename = Path.GetFileNameWithoutExtension(file); + + if(!filename.StartsWith("company_", StringComparison.InvariantCulture)) continue; + + string[] pieces = filename.Split('_'); + + if(pieces.Length != 3) continue; + + var guid = Guid.NewGuid(); + + if(!int.TryParse(pieces[1], out int companyId)) continue; + + if(!int.TryParse(pieces[2], out int year)) continue; + + try + { + context.CompanyLogos.Add(new CompanyLogo + { + CompanyId = companyId, + Year = year, + Guid = guid + }); + + context.SaveChanges(); + } + catch(Exception) + { + continue; + } + + SKSvg svg = null; + + foreach(int minSize in new[] + { + 256, 32 + }) + { + foreach(string format in new[] + { + "png", "webp" + }) + { + string outDir = minSize == 32 + ? Path.Combine("wwwroot/assets/logos/thumbs", format) + : Path.Combine("wwwroot/assets/logos", format); + + if(!Directory.Exists(outDir)) Directory.CreateDirectory(outDir); + + SKEncodedImageFormat skFormat; + + switch(format) + { + case "webp": + skFormat = SKEncodedImageFormat.Webp; + + break; + default: + skFormat = SKEncodedImageFormat.Png; + + break; + } + + foreach(int multiplier in new[] + { + 1, 2, 3 + }) + { + string outPath = Path.Combine(outDir, $"{multiplier}x"); + + if(!Directory.Exists(outPath)) Directory.CreateDirectory(outPath); + + string rendered = Path.Combine(outPath, $"{guid}.{format}"); + + if(File.Exists(rendered)) continue; + + Console.WriteLine("Rendering {0}", rendered); + + if(svg == null) + { + svg = new SKSvg(); + svg.Load(file); + } + + var outFs = new FileStream(rendered, FileMode.CreateNew); + RenderSvg(svg, outFs, skFormat, minSize, multiplier); + outFs.Close(); + } + } + } + + File.Move(file, $"wwwroot/assets/logos/{guid}.svg"); + } + } + + public static void RenderSvg(SKSvg svg, Stream outStream, SKEncodedImageFormat skFormat, int minSize, + int multiplier) + { + SKRect svgSize = svg.Picture.CullRect; + float svgMax = Math.Max(svgSize.Width, svgSize.Height); + float canvasMin = minSize * multiplier; + float scale = canvasMin / svgMax; + var matrix = SKMatrix.CreateScale(scale, scale); + var bitmap = new SKBitmap((int)(svgSize.Width * scale), (int)(svgSize.Height * scale)); + var canvas = new SKCanvas(bitmap); + canvas.Clear(); + canvas.DrawPicture(svg.Picture, ref matrix); + canvas.Flush(); + var image = SKImage.FromBitmap(bitmap); + SKData data = image.Encode(skFormat, 100); + data.SaveTo(outStream); + } + + // TODO: Reduce code duplication + public static void RenderCompanyLogo(Guid guid, Stream svgStream, string wwwroot) + { + SKSvg svg = null; + + foreach(int minSize in new[] + { + 256, 32 + }) + { + foreach(string format in new[] + { + "png", "webp" + }) + { + string outDir = minSize == 32 + ? Path.Combine(wwwroot, "assets/logos/thumbs", format) + : Path.Combine(wwwroot, "assets/logos", format); + + if(!Directory.Exists(outDir)) Directory.CreateDirectory(outDir); + + SKEncodedImageFormat skFormat; + + switch(format) + { + case "webp": + skFormat = SKEncodedImageFormat.Webp; + + break; + default: + skFormat = SKEncodedImageFormat.Png; + + break; + } + + foreach(int multiplier in new[] + { + 1, 2, 3 + }) + { + string outPath = Path.Combine(outDir, $"{multiplier}x"); + + if(!Directory.Exists(outPath)) Directory.CreateDirectory(outPath); + + string rendered = Path.Combine(outPath, $"{guid}.{format}"); + + if(File.Exists(rendered)) continue; + + Console.WriteLine("Rendering {0}", rendered); + + if(svg == null) + { + svg = new SKSvg(); + svg.Load(svgStream); + } + + var outFs = new FileStream(rendered, FileMode.CreateNew); + RenderSvg(svg, outFs, skFormat, minSize, multiplier); + outFs.Close(); + } + } + } + } +} \ No newline at end of file diff --git a/Marechai.Server/Interop/DetectOS.cs b/Marechai.Server/Interop/DetectOS.cs index 66c3e4b4..61cd2c79 100644 --- a/Marechai.Server/Interop/DetectOS.cs +++ b/Marechai.Server/Interop/DetectOS.cs @@ -41,7 +41,7 @@ using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; -namespace DiscImageChef.Interop; +namespace Marechai.Server.Interop; public static class DetectOS { diff --git a/Marechai.Server/Interop/PlatformID.cs b/Marechai.Server/Interop/PlatformID.cs index af190967..6a13ca11 100644 --- a/Marechai.Server/Interop/PlatformID.cs +++ b/Marechai.Server/Interop/PlatformID.cs @@ -36,7 +36,7 @@ // Copyright © 2011-2018 Natalia Portillo // ****************************************************************************/ -namespace DiscImageChef.Interop; +namespace Marechai.Server.Interop; /// Contains an arbitrary list of OSes, even if .NET does not run on them public enum PlatformID diff --git a/Marechai.Server/Interop/Version.cs b/Marechai.Server/Interop/Version.cs index 4d6b9cf1..f3c38efa 100644 --- a/Marechai.Server/Interop/Version.cs +++ b/Marechai.Server/Interop/Version.cs @@ -40,7 +40,7 @@ using System; using System.Reflection; using System.Runtime; -namespace DiscImageChef.Interop; +namespace Marechai.Server.Interop; public static class Version { @@ -52,10 +52,7 @@ public static class Version { Assembly assembly = typeof(GCSettings).Assembly; - string[] assemblyPath = assembly.Location.Split([ - '/', '\\' - ], - StringSplitOptions.RemoveEmptyEntries); + string[] assemblyPath = assembly.Location.Split(['/', '\\'], StringSplitOptions.RemoveEmptyEntries); int netCoreAppIndex = Array.IndexOf(assemblyPath, "Microsoft.NETCore.App"); diff --git a/Marechai.Server/Marechai.Server.csproj b/Marechai.Server/Marechai.Server.csproj index 9d0d97c8..fb298ef3 100644 --- a/Marechai.Server/Marechai.Server.csproj +++ b/Marechai.Server/Marechai.Server.csproj @@ -1,11 +1,18 @@ - - net10.0 - + + net10.0 + - - - + + + + + + + + + + diff --git a/Marechai.Server/Program.cs b/Marechai.Server/Program.cs index 3ebdd501..3ebbc470 100644 --- a/Marechai.Server/Program.cs +++ b/Marechai.Server/Program.cs @@ -1,14 +1,147 @@ +using System; +using System.Linq; +using Aaru.CommonTypes.Interop; +using Marechai.Database; +using Marechai.Database.Models; +using Marechai.Helpers; +using Marechai.Server.Helpers; +using Markdig; using Microsoft.AspNetCore.Builder; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Version = Marechai.Server.Interop.Version; namespace Marechai.Server; -public class Program +file class Program { + static IDbCore _database; + public static void Main(string[] args) { - var builder = WebApplication.CreateBuilder(args); + Console.Clear(); + + Console.Write("\e[32m . ,,\n" + + "\e[32m ;,. '0d.\n" + + "\e[32m oc oWd \e[31m" + + @"________/\\\\\\\\\___/\\\\\\\\\\\_________/\\\\\\\\\___/\\\\____________/\\\\_" + + "\n\e[0m" + + "\e[32m ;X. 'WN' \e[31m" + + @" _____/\\\////////___\/////\\\///_______/\\\////////___\/\\\\\\________/\\\\\\_" + + "\n\e[0m" + + "\e[32m oMo cMM: \e[31m" + + @" ___/\\\/________________\/\\\________/\\\/____________\/\\\//\\\____/\\\//\\\_" + + "\n\e[0m" + + "\e[32m ;MM. .MMM; \e[31m" + + @" __/\\\__________________\/\\\_______/\\\______________\/\\\\///\\\/\\\/_\/\\\_" + + "\n\e[0m" + + "\e[32m NMM WMMW \e[31m" + + @" _\/\\\__________________\/\\\______\/\\\______________\/\\\__\///\\\/___\/\\\_" + + "\n\e[0m" + + "\e[32m 'MMM MMMM; \e[31m" + + @" _\//\\\_________________\/\\\______\//\\\_____________\/\\\____\///_____\/\\\_" + + "\n\e[0m" + + "\e[32m ,MMM: dMMMM: \e[31m" + + @" __\///\\\_______________\/\\\_______\///\\\___________\/\\\_____________\/\\\_" + + "\n\e[0m" + + "\e[32m .MMMW. :MMMMM. \e[31m" + + @" ____\////\\\\\\\\\___/\\\\\\\\\\\_____\////\\\\\\\\\__\/\\\_____________\/\\\_" + + "\n\e[0m" + + "\e[32m XMMMW: .:xKNMMMMMMN0d, lMMMMMd \e[31m" + + @" _______\/////////___\///////////_________\/////////___\///______________\///__" + + "\n\e[0m" + + "\e[32m :MMMMMK; cWMNkl:;;;:lxKMXc .0MMMMMO \e[37;1m MARECHAI\e[0m\n" + + "\e[32m ..KMMMMMMNo,. ,OMMMMMMW:,. \e[37;1m Master repository of computing history artifacts information\e[0m\n" + + "\e[32m .;d0NMMMMMMMMMMMMMMW0d:' .;lOWMMMMMMMMMMMMMXkl. \e[37;1m Version \e[0m\e[33m{0}\e[37;1m-\e[0m\e[31m{1}\e[0m\n" + + "\e[32m :KMMMMMMMMMMMMMMMMMMMMMMMMc WMMMMMMMMMMMMMMMMMMMMMMWk'\e[0m\n" + + "\e[32m ;NMMMMWX0kkkkO0XMMMMMMMMMMM0' dNMMMMMMMMMMW0xl:;,;:oOWMMX; \e[37;1m Running under \e[35;1m{2}\e[37;1m, \e[35m{3}-bit\e[37;1m in \e[35m{4}-bit\e[37;1m mode.\e[0m\n" + + "\e[32m xMMWk:. .c0MMMMMW' OMMMMMM0c'.. .oNMO \e[37;1m Using \e[33;1m{5}\e[37;1m version \e[31;1m{6}\e[0m\n" + + "\e[32m OMNc .MNc oWMMk 'WMMNl. .MMK ;KX.\e[0m\n" + + "\e[32m xMO WMN ; ., , ': ,MMx lK\e[0m\n" + + "\e[32m ,Md cMMl .XMMMWWMMMO XMW. :\e[0m\n" + + "\e[32m Ok xMMl XMMMMMMMMc 0MW,\e[0m\n" + + "\e[32m 0 oMM0' lMMMMMMMM. :NMN'\e[0m\n" + + "\e[32m . .0MMKl ;MMMMMMMM oNMWd\e[0m\n" + + "\e[32m .dNW cMMMMMMMM, XKl\e[0m\n" + + "\e[32m 0MMMMMMMMK\e[0m\n" + + "\e[32m ;MMMMMMMMMMO \e[37;1m Proudly presented to you by:\e[0m\n" + + "\e[32m 'WMMMMKxMMMMM0 \e[34;1m Natalia Portillo\e[0m\n" + + "\e[32m oMMMMNc :WMMMMN:\e[0m\n" + + "\e[32m .dWMMM0; dWMMMMXl. \e[37;1m Thanks to all contributors, collaborators, translators, donators and friends.\e[0m\n" + + "\e[32m .......,cd0WMMNk: c0MMMMMWKkolc:clodc'\e[0m\n" + + "\e[32m .';loddol:'. ':oxkkOkkxoc,.\e[0m\n" + + "\e[0m\n", + Version.GetVersion(), +#if DEBUG + "DEBUG" +#else + "RELEASE" +#endif + , + DetectOS.GetPlatformName(DetectOS.GetRealPlatformID()), + Environment.Is64BitOperatingSystem ? 64 : 32, + Environment.Is64BitProcess ? 64 : 32, + DetectOS.IsMono ? "Mono" : ".NET Core", + DetectOS.IsMono ? Version.GetMonoVersion() : Version.GetNetCoreVersion()); + + Console.WriteLine("\e[31;1mUpdating MySQL database without Entity Framework if it exists...\e[0m"); + _database = new Mysql(); + + IConfigurationBuilder configurationBuilder = new ConfigurationBuilder().AddJsonFile("appsettings.json"); + IConfigurationRoot configuration = configurationBuilder.Build(); + string connectionString = configuration.GetConnectionString("DefaultConnection"); + + if(connectionString is null) + Console.WriteLine("\e[31;1mCould not find a correct connection string...\e[0m"); + else + { + string server = null, user = null, database = null, password = null; + ushort port = 0; + string[] pieces = connectionString.Split(';'); + + foreach(string piece in pieces) + { + if(piece.StartsWith("server=", StringComparison.Ordinal)) + server = piece[7..]; + else if(piece.StartsWith("user=", StringComparison.Ordinal)) + user = piece[5..]; + else if(piece.StartsWith("password=", StringComparison.Ordinal)) + password = piece[9..]; + else if(piece.StartsWith("database=", StringComparison.Ordinal)) + database = piece[9..]; + else if(piece.StartsWith("port=", StringComparison.Ordinal)) + { + string portString = piece[5..]; + + ushort.TryParse(portString, out port); + } + } + + if(server is null || user is null || database is null || password is null || port == 0) + Console.WriteLine("\e[31;1mCould not find a correct connection string...\e[0m"); + + else + { + bool res = _database.OpenDb(server, user, database, password, port); + + if(res) + { + Console.WriteLine("\e[31;1mClosing database...\e[0m"); + _database.CloseDb(); + } + } + } + + DateTime start = DateTime.Now; + Console.WriteLine("\e[31;1mRendering new country flags...\e[0m"); + SvgRender.RenderCountries(); + DateTime end = DateTime.Now; + + Console.WriteLine("\e[31;1mTook \e[32;1m{0} seconds\e[31;1m...\e[0m", (end - start).TotalSeconds); + + WebApplicationBuilder builder = WebApplication.CreateBuilder(args); // Add services to the container. @@ -17,21 +150,88 @@ public class Program // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi builder.Services.AddOpenApi(); - var app = builder.Build(); + builder.Services.AddDbContextFactory(options => options.UseLazyLoadingProxies() + .UseMySql(builder.Configuration + .GetConnectionString("DefaultConnection"), + new + MariaDbServerVersion(new System. + Version(10, 5, 0)), + b => b.UseMicrosoftJson())); + + WebApplication app = builder.Build(); // Configure the HTTP request pipeline. - if(app.Environment.IsDevelopment()) - { - app.MapOpenApi(); - } + if(app.Environment.IsDevelopment()) app.MapOpenApi(); app.UseHttpsRedirection(); app.UseAuthorization(); - app.MapControllers(); + using(IServiceScope scope = app.Services.CreateScope()) + { + IServiceProvider services = scope.ServiceProvider; + + try + { + start = DateTime.Now; + Console.WriteLine("\e[31;1mUpdating database with Entity Framework...\e[0m"); + MarechaiContext context = services.GetRequiredService(); + context.Database.Migrate(); + end = DateTime.Now; + + Console.WriteLine("\e[31;1mTook \e[32;1m{0} seconds\e[31;1m...\e[0m", (end - start).TotalSeconds); + + start = DateTime.Now; + Console.WriteLine("\e[31;1mImporting company logos...\e[0m"); + SvgRender.ImportCompanyLogos(context); + end = DateTime.Now; + + Console.WriteLine("\e[31;1mTook \e[32;1m{0} seconds\e[31;1m...\e[0m", (end - start).TotalSeconds); + + start = DateTime.Now; + Console.WriteLine("\e[31;1mRendering markdown in company descriptions...\e[0m"); + MarkdownPipeline pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build(); + + foreach(CompanyDescription companyDescription in + context.CompanyDescriptions.Where(cd => cd.Html == null)) + { + companyDescription.Html = Markdown.ToHtml(companyDescription.Text, pipeline); + context.Update(companyDescription); + } + + context.SaveChanges(); + + end = DateTime.Now; + + Console.WriteLine("\e[31;1mTook \e[32;1m{0} seconds\e[31;1m...\e[0m", (end - start).TotalSeconds); + + start = DateTime.Now; + Console.WriteLine("\e[31;1mEnsuring photo folders exist...\e[0m"); + Photos.EnsureCreated("wwwroot", false, "machines"); + end = DateTime.Now; + + start = DateTime.Now; + Console.WriteLine("\e[31;1mEnsuring scan folders exist...\e[0m"); + Photos.EnsureCreated("wwwroot", true, "books"); + Photos.EnsureCreated("wwwroot", true, "documents"); + Photos.EnsureCreated("wwwroot", true, "magazines"); + end = DateTime.Now; + + Console.WriteLine("\e[31;1mTook \e[32;1m{0} seconds\e[31;1m...\e[0m", (end - start).TotalSeconds); + } + catch(Exception ex) + { + Console.WriteLine("\e[31;1mCould not open database...\e[0m"); +#if DEBUG + Console.WriteLine("\e[31;1mException: {0}\e[0m", ex.Message); +#endif + return; + } + } + + Console.WriteLine("\e[31;1mStarting API server...\e[0m"); app.Run(); } } \ No newline at end of file diff --git a/Marechai.Server/WeatherForecast.cs b/Marechai.Server/WeatherForecast.cs index 811acb82..f1f2e618 100644 --- a/Marechai.Server/WeatherForecast.cs +++ b/Marechai.Server/WeatherForecast.cs @@ -1,3 +1,5 @@ +using System; + namespace Marechai.Server; public class WeatherForecast diff --git a/Marechai/Program.cs b/Marechai/Program.cs index eb93f0e7..58ba312b 100644 --- a/Marechai/Program.cs +++ b/Marechai/Program.cs @@ -24,217 +24,18 @@ *******************************************************************************/ using System; -using System.Linq; -using DiscImageChef.Interop; -using Marechai.Database; -using Marechai.Database.Models; -using Marechai.Helpers; -using Markdig; using Microsoft.AspNetCore.Hosting; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Version = DiscImageChef.Interop.Version; namespace Marechai; public static class Program { - internal static IDbCore _database; - public static void Main(string[] args) { - Console.Clear(); - - Console.Write("\u001b[32m . ,,\n" + - "\u001b[32m ;,. '0d.\n" + - "\u001b[32m oc oWd \u001b[31m" + - @"________/\\\\\\\\\___/\\\\\\\\\\\_________/\\\\\\\\\___/\\\\____________/\\\\_" + - "\n\u001b[0m" + - "\u001b[32m ;X. 'WN' \u001b[31m" + - @" _____/\\\////////___\/////\\\///_______/\\\////////___\/\\\\\\________/\\\\\\_" + - "\n\u001b[0m" + - "\u001b[32m oMo cMM: \u001b[31m" + - @" ___/\\\/________________\/\\\________/\\\/____________\/\\\//\\\____/\\\//\\\_" + - "\n\u001b[0m" + - "\u001b[32m ;MM. .MMM; \u001b[31m" + - @" __/\\\__________________\/\\\_______/\\\______________\/\\\\///\\\/\\\/_\/\\\_" + - "\n\u001b[0m" + - "\u001b[32m NMM WMMW \u001b[31m" + - @" _\/\\\__________________\/\\\______\/\\\______________\/\\\__\///\\\/___\/\\\_" + - "\n\u001b[0m" + - "\u001b[32m 'MMM MMMM; \u001b[31m" + - @" _\//\\\_________________\/\\\______\//\\\_____________\/\\\____\///_____\/\\\_" + - "\n\u001b[0m" + - "\u001b[32m ,MMM: dMMMM: \u001b[31m" + - @" __\///\\\_______________\/\\\_______\///\\\___________\/\\\_____________\/\\\_" + - "\n\u001b[0m" + - "\u001b[32m .MMMW. :MMMMM. \u001b[31m" + - @" ____\////\\\\\\\\\___/\\\\\\\\\\\_____\////\\\\\\\\\__\/\\\_____________\/\\\_" + - "\n\u001b[0m" + - "\u001b[32m XMMMW: .:xKNMMMMMMN0d, lMMMMMd \u001b[31m" + - @" _______\/////////___\///////////_________\/////////___\///______________\///__" + - "\n\u001b[0m" + - "\u001b[32m :MMMMMK; cWMNkl:;;;:lxKMXc .0MMMMMO \u001b[37;1m MARECHAI\u001b[0m\n" + - "\u001b[32m ..KMMMMMMNo,. ,OMMMMMMW:,. \u001b[37;1m Master repository of computing history artifacts information\u001b[0m\n" + - "\u001b[32m .;d0NMMMMMMMMMMMMMMW0d:' .;lOWMMMMMMMMMMMMMXkl. \u001b[37;1m Version \u001b[0m\u001b[33m{0}\u001b[37;1m-\u001b[0m\u001b[31m{1}\u001b[0m\n" + - "\u001b[32m :KMMMMMMMMMMMMMMMMMMMMMMMMc WMMMMMMMMMMMMMMMMMMMMMMWk'\u001b[0m\n" + - "\u001b[32m ;NMMMMWX0kkkkO0XMMMMMMMMMMM0' dNMMMMMMMMMMW0xl:;,;:oOWMMX; \u001b[37;1m Running under \u001b[35;1m{2}\u001b[37;1m, \u001b[35m{3}-bit\u001b[37;1m in \u001b[35m{4}-bit\u001b[37;1m mode.\u001b[0m\n" + - "\u001b[32m xMMWk:. .c0MMMMMW' OMMMMMM0c'.. .oNMO \u001b[37;1m Using \u001b[33;1m{5}\u001b[37;1m version \u001b[31;1m{6}\u001b[0m\n" + - "\u001b[32m OMNc .MNc oWMMk 'WMMNl. .MMK ;KX.\u001b[0m\n" + - "\u001b[32m xMO WMN ; ., , ': ,MMx lK\u001b[0m\n" + - "\u001b[32m ,Md cMMl .XMMMWWMMMO XMW. :\u001b[0m\n" + - "\u001b[32m Ok xMMl XMMMMMMMMc 0MW,\u001b[0m\n" + - "\u001b[32m 0 oMM0' lMMMMMMMM. :NMN'\u001b[0m\n" + - "\u001b[32m . .0MMKl ;MMMMMMMM oNMWd\u001b[0m\n" + - "\u001b[32m .dNW cMMMMMMMM, XKl\u001b[0m\n" + - "\u001b[32m 0MMMMMMMMK\u001b[0m\n" + - "\u001b[32m ;MMMMMMMMMMO \u001b[37;1m Proudly presented to you by:\u001b[0m\n" + - "\u001b[32m 'WMMMMKxMMMMM0 \u001b[34;1m Natalia Portillo\u001b[0m\n" + - "\u001b[32m oMMMMNc :WMMMMN:\u001b[0m\n" + - "\u001b[32m .dWMMM0; dWMMMMXl. \u001b[37;1m Thanks to all contributors, collaborators, translators, donators and friends.\u001b[0m\n" + - "\u001b[32m .......,cd0WMMNk: c0MMMMMWKkolc:clodc'\u001b[0m\n" + - "\u001b[32m .';loddol:'. ':oxkkOkkxoc,.\u001b[0m\n" + - "\u001b[0m\n", - Version.GetVersion(), -#if DEBUG - "DEBUG" -#else - "RELEASE" -#endif - , - DetectOS.GetPlatformName(DetectOS.GetRealPlatformID()), - Environment.Is64BitOperatingSystem ? 64 : 32, - Environment.Is64BitProcess ? 64 : 32, - DetectOS.IsMono ? "Mono" : ".NET Core", - DetectOS.IsMono ? Version.GetMonoVersion() : Version.GetNetCoreVersion()); - - Console.WriteLine("\u001b[31;1mUpdating MySQL database without Entity Framework if it exists...\u001b[0m"); - _database = new Mysql(); - - IConfigurationBuilder builder = new ConfigurationBuilder().AddJsonFile("appsettings.json"); - IConfigurationRoot configuration = builder.Build(); - string connectionString = configuration.GetConnectionString("DefaultConnection"); - - if(connectionString is null) - Console.WriteLine("\u001b[31;1mCould not find a correct connection string...\u001b[0m"); - else - { - string server = null, user = null, database = null, password = null; - ushort port = 0; - string[] pieces = connectionString.Split(";"); - - foreach(string piece in pieces) - { - if(piece.StartsWith("server=", StringComparison.Ordinal)) - server = piece.Substring(7); - else if(piece.StartsWith("user=", StringComparison.Ordinal)) - user = piece.Substring(5); - else if(piece.StartsWith("password=", StringComparison.Ordinal)) - password = piece.Substring(9); - else if(piece.StartsWith("database=", StringComparison.Ordinal)) - database = piece.Substring(9); - else if(piece.StartsWith("port=", StringComparison.Ordinal)) - { - string portString = piece.Substring(5); - - ushort.TryParse(portString, out port); - } - } - - if(server is null || user is null || database is null || password is null || port == 0) - Console.WriteLine("\u001b[31;1mCould not find a correct connection string...\u001b[0m"); - - else - { - bool res = _database.OpenDb(server, user, database, password, port); - - if(res) - { - Console.WriteLine("\u001b[31;1mClosing database...\u001b[0m"); - _database.CloseDb(); - } - } - } - - DateTime start = DateTime.Now; - Console.WriteLine("\u001b[31;1mRendering new country flags...\u001b[0m"); - SvgRender.RenderCountries(); - DateTime end = DateTime.Now; - - Console.WriteLine("\u001b[31;1mTook \u001b[32;1m{0} seconds\u001b[31;1m...\u001b[0m", - (end - start).TotalSeconds); - IHost host = BuildHost(args); - using(IServiceScope scope = host.Services.CreateScope()) - { - IServiceProvider services = scope.ServiceProvider; - - try - { - start = DateTime.Now; - Console.WriteLine("\u001b[31;1mUpdating database with Entity Framework...\u001b[0m"); - MarechaiContext context = services.GetRequiredService(); - context.Database.Migrate(); - end = DateTime.Now; - - Console.WriteLine("\u001b[31;1mTook \u001b[32;1m{0} seconds\u001b[31;1m...\u001b[0m", - (end - start).TotalSeconds); - - start = DateTime.Now; - Console.WriteLine("\u001b[31;1mImporting company logos...\u001b[0m"); - SvgRender.ImportCompanyLogos(context); - end = DateTime.Now; - - Console.WriteLine("\u001b[31;1mTook \u001b[32;1m{0} seconds\u001b[31;1m...\u001b[0m", - (end - start).TotalSeconds); - - start = DateTime.Now; - Console.WriteLine("\u001b[31;1mRendering markdown in company descriptions...\u001b[0m"); - MarkdownPipeline pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build(); - - foreach(CompanyDescription companyDescription in - context.CompanyDescriptions.Where(cd => cd.Html == null)) - { - companyDescription.Html = Markdown.ToHtml(companyDescription.Text, pipeline); - context.Update(companyDescription); - } - - context.SaveChanges(); - - end = DateTime.Now; - - Console.WriteLine("\u001b[31;1mTook \u001b[32;1m{0} seconds\u001b[31;1m...\u001b[0m", - (end - start).TotalSeconds); - - start = DateTime.Now; - Console.WriteLine("\u001b[31;1mEnsuring photo folders exist...\u001b[0m"); - Photos.EnsureCreated("wwwroot", false, "machines"); - end = DateTime.Now; - - start = DateTime.Now; - Console.WriteLine("\u001b[31;1mEnsuring scan folders exist...\u001b[0m"); - Photos.EnsureCreated("wwwroot", true, "books"); - Photos.EnsureCreated("wwwroot", true, "documents"); - Photos.EnsureCreated("wwwroot", true, "magazines"); - end = DateTime.Now; - - Console.WriteLine("\u001b[31;1mTook \u001b[32;1m{0} seconds\u001b[31;1m...\u001b[0m", - (end - start).TotalSeconds); - } - catch(Exception ex) - { - Console.WriteLine("\u001b[31;1mCould not open database...\u001b[0m"); -#if DEBUG - Console.WriteLine("\u001b[31;1mException: {0}\u001b[0m", ex.Message); -#endif - return; - } - } - - Console.WriteLine("\u001b[31;1mStarting web server...\u001b[0m"); + Console.WriteLine("\e[31;1mStarting web server...\e[0m"); host.Run(); }