using Microsoft.AspNetCore.StaticFiles; using System.Security.Cryptography; using System.Text; namespace Lux.API.Controllers { [Route("api/[controller]")] [ApiController] public class ImageController : ControllerBase { #region Public Constructors public ImageController(IConfiguration config, IImageCacheService imgServ, ILogger logger) { _config = config; _imgService = imgServ; _logger = logger; // setup params basePath = _config.GetValue("ServerConf:FileSharePath") ?? "unsafe_uploads"; } #endregion Public Constructors #region Public Methods /// /// Chiamata GET: restituisce file SVG/PNG (da file o cache REDIS), eliminando nome rand (x force refresh) /// GET: api/image/OFF0000001.001.svg?env=WINDOW /// GET: api/image/cache/OFF0000001.001.svg?env=WINDOW /// GET: api/image/OFF0000002.001.png?env=WINDOW /// GET: api/image/cache/OFF0000002.001.png?env=WINDOW /// GET: api/image/OFF0000002.002-123456.png?env=WINDOW /// GET: api/image/cache/OFF0000002.002-123456.png?env=WINDOW /// /// uid oggetto /// environment oggetto /// [HttpGet("{id}")] [HttpGet("cache/{id}")] public async Task cacheFile(string id, EgwMultiEngineManager.Data.Constants.EXECENVIRONMENTS env = EgwMultiEngineManager.Data.Constants.EXECENVIRONMENTS.WINDOW) { Stopwatch sw = new Stopwatch(); sw.Start(); if (string.IsNullOrEmpty(id)) return NotFound(); string mimeType = "txt"; byte[] bytes = new byte[0]; // ...se ricevo percorso --> leggo jwd/svg cablato if (!string.IsNullOrEmpty(id)) { // se contiene i caratteri casuali x forzare reload --> li levo if (id.Contains("-")) { id = id.Substring(0, id.IndexOf("-")); } // secondo del tipo + envr decodifico valore corretto switch (env) { case Constants.EXECENVIRONMENTS.NULL: break; case Constants.EXECENVIRONMENTS.WINDOW: mimeType = "image/svg+xml"; string svgContent = await _imgService.LoadSvgAsync(id, env); // se vuoto --> leggo img logo... if (string.IsNullOrEmpty(svgContent)) { string filePath = Path.Combine("DemoImg", "LogoEgalware.svg"); svgContent = await System.IO.File.ReadAllTextAsync(filePath); } bytes = Encoding.UTF8.GetBytes(svgContent); break; case Constants.EXECENVIRONMENTS.BEAM: case Constants.EXECENVIRONMENTS.WALL: case Constants.EXECENVIRONMENTS.CABINET: default: mimeType = "image/png"; string base64Encoded = _imgService.LoadPng(id, EgwMultiEngineManager.Data.Constants.EXECENVIRONMENTS.BEAM); // converto base64 bytes = Convert.FromBase64String(base64Encoded); break; } } sw.Stop(); Log.Info($"{mimeType} | {sw.Elapsed.TotalMilliseconds:N3} ms"); return File(bytes, mimeType); } /// /// Chiamata GET: restituisce file PNG (da file o da cache) /// PUT: api/image/png/00000000-0000-0000-0000-000000000000 /// /// id oggetto /// [HttpGet("png/{id}")] public async Task png(string id) { Stopwatch sw = new Stopwatch(); sw.Start(); string base64Encoded = ""; byte[] decodedBytes = new byte[0]; // ...se ricevo percorso --> leggo jwd/svg cablato if (!string.IsNullOrEmpty(id)) { // bonifica nome svg da base64Encoded = _imgService.LoadPng(id, EgwMultiEngineManager.Data.Constants.EXECENVIRONMENTS.BEAM); // converto base64 decodedBytes = Convert.FromBase64String(base64Encoded); } sw.Stop(); Log.Info($"pngString | {sw.Elapsed.TotalMilliseconds:N3} ms"); //return Ok(decodedString); return File(decodedBytes, "image/png"); } /// /// Chiamata GET: restituisce file SVG/PNG (da file, area static) /// GET: api/image/static/SP.000000000005.jpg /// GET: api/image/static/SP.000000000006.svg /// /// uid oggetto /// [HttpGet("static/{id}")] public async Task StaticFile(string id) { Stopwatch sw = new Stopwatch(); sw.Start(); if (string.IsNullOrWhiteSpace(id)) return NotFound(); string filePath = Path.Combine(basePath, "static", id); if (!System.IO.File.Exists(filePath)) { // uso empy.svg! id = "empty.svg"; filePath = Path.Combine(basePath, "static", id); } if (!System.IO.File.Exists(filePath)) { //se non ci fosse errore... return NotFound(); } // MIME automatico var provider = new FileExtensionContentTypeProvider(); if (!provider.TryGetContentType(filePath, out string mimeType)) mimeType = "application/octet-stream"; // Last-Modified var lastModified = System.IO.File.GetLastWriteTimeUtc(filePath); Response.Headers["Last-Modified"] = lastModified.ToString("R"); // Cache-Control (1 giorno) Response.Headers["Cache-Control"] = "public,max-age=86400"; // ETag string etag = GenerateETag(filePath); Response.Headers["ETag"] = etag; // Se il client ha già la versione aggiornata → 304 if (Request.Headers.TryGetValue("If-None-Match", out var inm) && inm.ToString() == etag) { return StatusCode(StatusCodes.Status304NotModified); } // Se il client usa If-Modified-Since if (Request.Headers.TryGetValue("If-Modified-Since", out var ims) && DateTime.TryParse(ims, out var since) && Math.Abs((lastModified - since.ToUniversalTime()).TotalSeconds) < 1) { return StatusCode(StatusCodes.Status304NotModified); } // Streaming ottimizzato (range support) var stream = System.IO.File.OpenRead(filePath); sw.Stop(); Log.Info($"{mimeType} | {sw.Elapsed.TotalMilliseconds:N3} ms"); return File(stream, mimeType, enableRangeProcessing: true); } /// /// Chiamata GET: riceve Json in formato JwdDto, restituisce svg file /// GET: api/image/svg/00000000-0000-0000-0000-000000000000 /// /// id univoco img /// [HttpGet("svg/{id}")] public async Task svgFileGet(string id) { Stopwatch sw = new Stopwatch(); sw.Start(); string filePath = Path.Combine("DemoImg", "AntaDoppia.svg"); var svgContent = await System.IO.File.ReadAllTextAsync(filePath); var bytes = System.Text.Encoding.UTF8.GetBytes(svgContent); sw.Stop(); _logger.LogInformation($"svgString | {sw.Elapsed.TotalMilliseconds:N3} ms"); return File(bytes, "image/svg+xml"); } [HttpGet("version")] public string version() { var version = Assembly .GetExecutingAssembly() .GetName() .Version? .ToString() ?? "unknown"; return version; } #endregion Public Methods #region Private Fields private static Logger Log = LogManager.GetCurrentClassLogger(); private readonly IImageCacheService _imgService; private readonly ILogger _logger; private IConfiguration _config; /// /// Base path x network share files /// private string basePath = "unsafe_uploads"; #endregion Private Fields #region Private Methods /// /// ETag semplice basato su hash /// /// /// private string GenerateETag(byte[] data) { using var sha = System.Security.Cryptography.SHA256.Create(); var hash = sha.ComputeHash(data); return "\"" + Convert.ToBase64String(hash) + "\""; } private string GenerateETag(string filePath) { using var sha = SHA256.Create(); using var stream = System.IO.File.OpenRead(filePath); var hash = sha.ComputeHash(stream); return "\"" + Convert.ToBase64String(hash) + "\""; } #endregion Private Methods } }