using System; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text; using API.Services.Store; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using MimeTypes; namespace API.Controllers; #nullable enable [Authorize] [ApiController] [Route("api/[controller]")] public class BaseApiController : ControllerBase { /// /// Gets the current user context. Available in all derived controllers. /// protected IUserContext UserContext => field ??= HttpContext.RequestServices.GetRequiredService(); /// /// Gets the current authenticated user's ID. /// Throws if user is not authenticated. /// protected int UserId => UserContext.GetUserIdOrThrow(); /// /// Gets the current authenticated user's username. /// /// Warning! Username's can contain .. and /, do not use folders or filenames explicitly with the Username protected string? Username => UserContext.GetUsername(); /// /// Returns a physical file with proper HTTP caching headers and ETag support. /// Automatically handles conditional requests (If-None-Match) returning 304 Not Modified when appropriate. /// /// The absolute path to the file on disk. /// Cache duration in seconds. Default is 86400 (1 day). /// /// if path is null/empty or file doesn't exist. /// with 304 if client's cached version is current. /// with the file content and caching headers otherwise. /// protected ActionResult CachedFile(string? path, int maxAge = 86400) { if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return NotFound(); var lastWrite = System.IO.File.GetLastWriteTimeUtc(path); var etag = $"\"{lastWrite.Ticks:x}-{path.GetHashCode():x}\""; if (Request.Headers.IfNoneMatch.ToString() == etag) return StatusCode(304); Response.Headers.ETag = etag; Response.Headers.CacheControl = $"private, max-age={maxAge}"; var contentType = MimeTypeMap.GetMimeType(Path.GetExtension(path)); return PhysicalFile(path, contentType, Path.GetFileName(path), enableRangeProcessing: true); } /// /// Returns a file from byte[] content with proper HTTP caching headers and ETag support. /// ETag is generated from SHA256 hash of the content. /// Automatically handles conditional requests (If-None-Match) returning 304 Not Modified when appropriate. /// /// The file content as byte array. /// The MIME type of the content. /// Optional filename for Content-Disposition header. /// Cache duration in seconds. Default is 86400 (1 day). /// /// if content is null or empty. /// with 304 if client's cached version is current. /// with the content and caching headers otherwise. /// protected ActionResult CachedContent(byte[]? content, string contentType, string? fileName = null, int maxAge = 86400) { if (content is not { Length: > 0 }) return NotFound(); var etag = $"\"{Convert.ToHexString(SHA256.HashData(content))}\""; if (Request.Headers.IfNoneMatch.ToString() == etag) return StatusCode(304); Response.Headers.ETag = etag; Response.Headers.CacheControl = $"private, max-age={maxAge}"; return fileName is not null ? File(content, contentType, fileName) : File(content, contentType); } }