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);
}
}