mirror of
https://github.com/Kareadita/Kavita.git
synced 2026-01-14 16:10:22 -05:00
Co-authored-by: Amelia <77553571+Fesaa@users.noreply.github.com> Co-authored-by: Weblate (bot) <hosted@weblate.org> Co-authored-by: Adam Havránek <adamhavra@seznam.cz> Co-authored-by: Aindriú Mac Giolla Eoin <aindriu80@gmail.com> Co-authored-by: Dark77 <Dark77@pobox.sk> Co-authored-by: Frozehunter <frozehunter@me.com> Co-authored-by: Havokdan <havokdan@yahoo.com.br> Co-authored-by: Igor Dobrača <igor.dobraca@gmail.com> Co-authored-by: Karl B <karl.owl@proton.me> Co-authored-by: Morhain Olivier <sesram@users.noreply.hosted.weblate.org> Co-authored-by: daydreamrabbit <devrabbit90@gmail.com> Co-authored-by: karigane <169052233+karigane-cha@users.noreply.github.com> Co-authored-by: oxygen44k <iiccpp@outlook.com> Co-authored-by: Максим Горпиніч <gorpinicmaksim0@gmail.com> Co-authored-by: 無情天 <kofzhanganguo@126.com> Co-authored-by: 안세훈 <on9686@gmail.com>
114 lines
4.6 KiB
C#
114 lines
4.6 KiB
C#
using System;
|
|
using System.Diagnostics;
|
|
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
|
|
{
|
|
/// <summary>
|
|
/// Gets the current user context. Available in all derived controllers.
|
|
/// </summary>
|
|
protected IUserContext UserContext =>
|
|
field ??= HttpContext.RequestServices.GetRequiredService<IUserContext>();
|
|
|
|
/// <summary>
|
|
/// Gets the current authenticated user's ID.
|
|
/// Throws if user is not authenticated.
|
|
/// </summary>
|
|
protected int UserId => UserContext.GetUserIdOrThrow();
|
|
|
|
/// <summary>
|
|
/// Gets the current authenticated user's username.
|
|
/// </summary>
|
|
/// <remarks>Warning! Username's can contain .. and /, do not use folders or filenames explicitly with the Username</remarks>
|
|
protected string? Username => UserContext.GetUsername();
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <remarks>This will create a waterfall of cache validation checks if used in virtual scroller</remarks>
|
|
/// <param name="path">The absolute path to the file on disk.</param>
|
|
/// <param name="maxAge">Cache duration in seconds. Default is 300 (5 minutes).</param>
|
|
/// <returns>
|
|
/// <see cref="NotFoundResult"/> if path is null/empty or file doesn't exist.
|
|
/// <see cref="StatusCodeResult"/> with 304 if client's cached version is current.
|
|
/// <see cref="PhysicalFileResult"/> with the file content and caching headers otherwise.
|
|
/// </returns>
|
|
protected ActionResult CachedFile(string? path, int maxAge = 300)
|
|
{
|
|
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.Any(t => t == etag)) return StatusCode(304);
|
|
|
|
Response.Headers.ETag = etag;
|
|
Response.Headers.CacheControl = $"private, max-age={maxAge}, stale-while-revalidate={maxAge}";
|
|
|
|
var contentType = MimeTypeMap.GetMimeType(Path.GetExtension(path));
|
|
return PhysicalFile(path, contentType, Path.GetFileName(path), enableRangeProcessing: true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a physical file
|
|
/// </summary>
|
|
/// <param name="path"></param>
|
|
/// <returns></returns>
|
|
protected ActionResult PhysicalFile(string? path)
|
|
{
|
|
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return NotFound();
|
|
|
|
var contentType = MimeTypeMap.GetMimeType(Path.GetExtension(path));
|
|
return PhysicalFile(path, contentType, Path.GetFileName(path), enableRangeProcessing: true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="content">The file content as byte array.</param>
|
|
/// <param name="contentType">The MIME type of the content.</param>
|
|
/// <param name="fileName">Optional filename for Content-Disposition header.</param>
|
|
/// <param name="maxAge">Cache duration in seconds. Default is 86400 (1 day).</param>
|
|
/// <returns>
|
|
/// <see cref="NotFoundResult"/> if content is null or empty.
|
|
/// <see cref="StatusCodeResult"/> with 304 if client's cached version is current.
|
|
/// <see cref="FileContentResult"/> with the content and caching headers otherwise.
|
|
/// </returns>
|
|
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);
|
|
}
|
|
|
|
}
|