Files
ZelWiki/TightWiki/Controllers/FileController.cs
2025-02-07 16:16:10 +08:00

482 lines
21 KiB
C#

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using NTDLS.Helpers;
using SixLabors.ImageSharp;
using System.Web;
using TightWiki.Caching;
using TightWiki.Library;
using TightWiki.Models;
using TightWiki.Models.DataModels;
using TightWiki.Models.ViewModels.File;
using TightWiki.Repository;
using static TightWiki.Library.Images;
namespace TightWiki.Controllers
{
[Route("File")]
public class FileController(SignInManager<IdentityUser> signInManager, UserManager<IdentityUser> userManager)
: WikiControllerBase(signInManager, userManager)
{
/// <summary>
/// 获取附加到页面的图像
/// </summary>
/// <param name="givenPageNavigation">The navigation link of the page.</param>
/// <param name="givenFileNavigation">The navigation link of the file.</param>
/// <param name="fileRevision">The revision of the the file (NOT THE PAGE REVISION).</param>
/// <returns></returns>
[HttpGet("Image/{givenPageNavigation}/{givenFileNavigation}/{fileRevision:int?}")]
public ActionResult Image(string givenPageNavigation, string givenFileNavigation, int? fileRevision = null)
{
var pageNavigation = new NamespaceNavigation(givenPageNavigation);
var fileNavigation = new NamespaceNavigation(givenFileNavigation);
int givenScale = GetQueryValue("Scale", 100);
var cacheKey = WikiCacheKeyFunction.Build(WikiCache.Category.Page, [givenPageNavigation, givenFileNavigation, fileRevision, givenScale]);
if (WikiCache.TryGet<ImageCacheItem>(cacheKey, out var cached))
{
return File(cached.Bytes, cached.ContentType);
}
var file = PageFileRepository.GetPageFileAttachmentByPageNavigationFileRevisionAndFileNavigation(pageNavigation.Canonical, fileNavigation.Canonical, fileRevision);
if (file != null)
{
if (file.ContentType == "image/x-icon")
{
//We do not handle the resizing of icon file. Maybe later....
return File(file.Data, file.ContentType);
}
var img = SixLabors.ImageSharp.Image.Load(new MemoryStream(file.Data));
if (givenScale > 500)
{
givenScale = 500;
}
if (givenScale != 100)
{
int width = (int)(img.Width * (givenScale / 100.0));
int height = (int)(img.Height * (givenScale / 100.0));
//Adjusting by a ratio (and especially after applying additional scaling) may have caused one
// dimension to become very small (or even negative). So here we will check the height and width
// to ensure they are both at least n pixels and adjust both dimensions.
if (height < 16)
{
int difference = 16 - height;
height += difference;
width += difference;
}
if (width < 16)
{
int difference = 16 - width;
height += difference;
width += difference;
}
if (file.ContentType.Equals("image/gif", StringComparison.InvariantCultureIgnoreCase))
{
var resized = ResizeGifImage(file.Data, width, height);
return File(resized, "image/gif");
}
else
{
using var image = ResizeImage(img, width, height);
using var ms = new MemoryStream();
file.ContentType = BestEffortConvertImage(image, ms, file.ContentType);
var cacheItem = new ImageCacheItem(ms.ToArray(), file.ContentType);
WikiCache.Put(cacheKey, cacheItem);
return File(cacheItem.Bytes, cacheItem.ContentType);
}
}
else
{
return File(file.Data, file.ContentType);
}
}
else
{
return NotFound($"[{fileNavigation}] 无法在页面 [{pageNavigation}] 中找到.");
}
}
/// <summary>
/// Gets an image from the database, converts it to a PNG with optional scaling and returns it to the client.
/// </summary>
/// <param name="givenPageNavigation">The navigation link of the page.</param>
/// <param name="givenFileNavigation">The navigation link of the file.</param>
/// <param name="fileRevision">The revision of the the FILE (NOT THE PAGE REVISION)</param>
/// <returns></returns>
[AllowAnonymous]
[HttpGet("Png/{givenPageNavigation}/{givenFileNavigation}/{fileRevision:int?}")]
public ActionResult Png(string givenPageNavigation, string givenFileNavigation, int? fileRevision = null)
{
SessionState.RequireViewPermission();
var pageNavigation = new NamespaceNavigation(givenPageNavigation);
var fileNavigation = new NamespaceNavigation(givenFileNavigation);
int givenScale = GetQueryValue("Scale", 100);
var cacheKey = WikiCacheKeyFunction.Build(WikiCache.Category.Page, [givenPageNavigation, givenFileNavigation, fileRevision, givenScale]);
if (WikiCache.TryGet<ImageCacheItem>(cacheKey, out var cached))
{
return File(cached.Bytes, cached.ContentType);
}
var file = PageFileRepository.GetPageFileAttachmentByPageNavigationFileRevisionAndFileNavigation(pageNavigation.Canonical, fileNavigation.Canonical, fileRevision);
if (file != null)
{
var img = SixLabors.ImageSharp.Image.Load(new MemoryStream(Utility.Decompress(file.Data)));
if (givenScale > 500)
{
givenScale = 500;
}
if (givenScale != 100)
{
int width = (int)(img.Width * (givenScale / 100.0));
int height = (int)(img.Height * (givenScale / 100.0));
//Adjusting by a ratio (and especially after applying additional scaling) may have caused one
// dimension to become very small (or even negative). So here we will check the height and width
// to ensure they are both at least n pixels and adjust both dimensions.
if (height < 16)
{
int difference = 16 - height;
height += difference;
width += difference;
}
if (width < 16)
{
int difference = 16 - width;
height += difference;
width += difference;
}
using var image = Images.ResizeImage(img, width, height);
using var ms = new MemoryStream();
image.SaveAsPng(ms);
var cacheItem = new ImageCacheItem(ms.ToArray(), "image/png");
WikiCache.Put(cacheKey, cacheItem);
return File(cacheItem.Bytes, cacheItem.ContentType);
}
else
{
using var ms = new MemoryStream();
img.SaveAsPng(ms);
var cacheItem = new ImageCacheItem(ms.ToArray(), "image/png");
WikiCache.Put(cacheKey, cacheItem);
return File(cacheItem.Bytes, cacheItem.ContentType);
}
}
else
{
return NotFound($"[{fileNavigation}] 无法在页面 [{pageNavigation}] 中找到.");
}
}
/// <summary>
/// Gets a file from the database and returns it to the client. This works even
/// when the file or file revision is not even attached to the page anymore.
/// <param name="givenPageNavigation">The navigation link of the page.</param>
/// <param name="givenFileNavigation">The navigation link of the file.</param>
/// <param name="fileRevision">The revision of the the FILE (NOT THE PAGE REVISION),</param>
/// </summary>
[AllowAnonymous]
[HttpGet("Binary/{givenPageNavigation}/{givenFileNavigation}/{fileRevision:int?}")]
public ActionResult Binary(string givenPageNavigation, string givenFileNavigation, int? fileRevision = null)
{
SessionState.RequireViewPermission();
var pageNavigation = new NamespaceNavigation(givenPageNavigation);
var fileNavigation = new NamespaceNavigation(givenFileNavigation);
var file = PageFileRepository.GetPageFileAttachmentByPageNavigationFileRevisionAndFileNavigation(pageNavigation.Canonical, fileNavigation.Canonical, fileRevision);
if (file != null)
{
return File(file.Data.ToArray(), file.ContentType);
}
else
{
HttpContext.Response.StatusCode = 404;
return NotFound($"[{fileNavigation}] 无法在页面 [ {pageNavigation} ] 中找到.");
}
}
/// <summary>
/// Populate the upload page. Shows the attachments.
/// </summary>
[Authorize]
[HttpGet("Revisions/{givenPageNavigation}/{givenFileNavigation}")]
public ActionResult Revisions(string givenPageNavigation, string givenFileNavigation)
{
SessionState.RequireViewPermission();
var pageNavigation = new NamespaceNavigation(givenPageNavigation);
var fileNavigation = new NamespaceNavigation(givenFileNavigation);
var model = new PageFileRevisionsViewModel()
{
PageNavigation = pageNavigation.Canonical,
FileNavigation = fileNavigation.Canonical,
Revisions = PageFileRepository.GetPageFileAttachmentRevisionsByPageAndFileNavigationPaged
(pageNavigation.Canonical, fileNavigation.Canonical, GetQueryValue("page", 1))
};
model.PaginationPageCount = (model.Revisions.FirstOrDefault()?.PaginationPageCount ?? 0);
return View(model);
}
/// <summary>
/// Populate the upload page. Shows the attachments.
/// </summary>
[Authorize]
[HttpGet("PageAttachments/{givenPageNavigation}")]
public ActionResult PageAttachments(string givenPageNavigation)
{
SessionState.RequireCreatePermission();
var pageNavigation = new NamespaceNavigation(givenPageNavigation);
var page = PageRepository.GetPageRevisionByNavigation(pageNavigation);
if (page != null)
{
var pageFiles = PageFileRepository.GetPageFilesInfoByPageId(page.Id);
return View(new FileAttachmentViewModel
{
PageNavigation = page.Navigation,
PageRevision = page.Revision,
Files = pageFiles
});
}
return View(new FileAttachmentViewModel
{
Files = new()
});
}
/// <summary>
/// Uploads a file by drag drop.
/// </summary>
[Authorize]
[HttpPost("UploadDragDrop/{givenPageNavigation}")]
public IActionResult UploadDragDrop(string givenPageNavigation, List<IFormFile> postedFiles)
{
SessionState.RequireCreatePermission();
try
{
var pageNavigation = new NamespaceNavigation(givenPageNavigation);
var page = PageRepository.GetPageInfoByNavigation(pageNavigation.Canonical).EnsureNotNull();
foreach (IFormFile file in postedFiles)
{
if (file != null)
{
var fileSize = file.Length;
if (fileSize > 0)
{
if (fileSize > GlobalConfiguration.MaxAttachmentFileSize)
{
return Json(new { message = $"无法保存文件: [{file.FileName}], 文件过大." });
}
var fileName = HttpUtility.UrlDecode(file.FileName);
PageFileRepository.UpsertPageFile(new PageFileAttachment()
{
Data = Utility.ConvertHttpFileToBytes(file),
CreatedDate = DateTime.UtcNow,
PageId = page.Id,
Name = fileName,
FileNavigation = Navigation.Clean(fileName),
Size = fileSize,
ContentType = Utility.GetMimeType(fileName)
}, (SessionState.Profile?.UserId).EnsureNotNullOrEmpty());
}
}
}
return Json(new { success = true, message = $"{postedFiles.Count:n0} file{(postedFiles.Count == 0 || postedFiles.Count > 1 ? "s" : string.Empty)}." });
}
catch (Exception ex)
{
ExceptionRepository.InsertException(ex, "文件上传失败.");
return StatusCode(500, new { success = false, message = $"Error: {ex.Message}" });
}
}
/// <summary>
/// Uploads a file by manually selecting it for upload.
/// </summary>
/// <param name="postData"></param>
/// <returns></returns>
[Authorize]
[HttpPost("ManualUpload/{givenPageNavigation}")]
public IActionResult ManualUpload(string givenPageNavigation, IFormFile fileData)
{
SessionState.RequireCreatePermission();
var pageNavigation = new NamespaceNavigation(givenPageNavigation);
var page = PageRepository.GetPageInfoByNavigation(pageNavigation.Canonical).EnsureNotNull();
if (fileData != null)
{
var fileSize = fileData.Length;
if (fileSize > 0)
{
if (fileSize > GlobalConfiguration.MaxAttachmentFileSize)
{
return Content("附件无法保存, 文件过大");
}
var fileName = HttpUtility.UrlDecode(fileData.FileName);
PageFileRepository.UpsertPageFile(new PageFileAttachment()
{
Data = Utility.ConvertHttpFileToBytes(fileData),
CreatedDate = DateTime.UtcNow,
PageId = page.Id,
Name = fileName,
FileNavigation = Navigation.Clean(fileName),
Size = fileSize,
ContentType = Utility.GetMimeType(fileName)
}, (SessionState.Profile?.UserId).EnsureNotNullOrEmpty());
return Content("成功");
}
}
return Content("失败");
}
/// <summary>
/// Allows a user to delete a page attachment from a page.
/// </summary>
/// <param name="navigation"></param>
/// <returns></returns>
[HttpPost("Detach/{givenPageNavigation}/{givenFileNavigation}/{pageRevision}")]
public ActionResult Detach(string givenPageNavigation, string givenFileNavigation, int pageRevision)
{
SessionState.RequireDeletePermission();
PageFileRepository.DetachPageRevisionAttachment(
new NamespaceNavigation(givenPageNavigation).Canonical,
new NamespaceNavigation(givenFileNavigation).Canonical, pageRevision);
return Content("Success");
}
/// <summary>
/// Gets a file from the database, converts it to a PNG with optional scaling and returns it to the client.
/// </summary>
/// <param name="navigation"></param>
/// <returns></returns>
[AllowAnonymous]
[HttpGet("Emoji/{givenPageNavigation}")]
public ActionResult Emoji(string givenPageNavigation)
{
SessionState.RequireViewPermission();
var pageNavigation = Navigation.Clean(givenPageNavigation);
if (string.IsNullOrEmpty(pageNavigation) == false)
{
string scale = GetQueryValue("Scale", "100");
string shortcut = $"%%{pageNavigation.ToLower()}%%";
var emoji = GlobalConfiguration.Emojis.Where(o => o.Shortcut == shortcut).FirstOrDefault();
if (emoji != null)
{
//Do we have this scale cached already?
var scaledImageCacheKey = WikiCacheKey.Build(WikiCache.Category.Emoji, [shortcut, scale]);
if (WikiCache.TryGet<ImageCacheItem>(scaledImageCacheKey, out var cachedEmoji))
{
return File(cachedEmoji.Bytes, cachedEmoji.ContentType);
}
var imageCacheKey = WikiCacheKey.Build(WikiCache.Category.Emoji, [shortcut]);
emoji.ImageData = WikiCache.Get<byte[]>(imageCacheKey);
if (emoji.ImageData == null)
{
//We don't get the bytes by default, that would be a lot of RAM for all the thousands of images.
emoji.ImageData = EmojiRepository.GetEmojiByName(emoji.Name)?.ImageData;
if (emoji.ImageData == null)
{
return NotFound($"Emoji {pageNavigation} 无法找到");
}
WikiCache.Put(imageCacheKey, emoji.ImageData);
}
if (emoji.ImageData != null)
{
var decompressedImageBytes = Utility.Decompress(emoji.ImageData);
var img = SixLabors.ImageSharp.Image.Load(new MemoryStream(decompressedImageBytes));
int customScalePercent = int.Parse(scale);
if (customScalePercent > 500)
{
customScalePercent = 500;
}
var (Width, Height) = Utility.ScaleToMaxOf(img.Width, img.Height, GlobalConfiguration.DefaultEmojiHeight);
//Adjust to any specified scaling.
Height = (int)(Height * (customScalePercent / 100.0));
Width = (int)(Width * (customScalePercent / 100.0));
//Adjusting by a ratio (and especially after applying additional scaling) may have caused one
// dimension to become very small (or even negative). So here we will check the height and width
// to ensure they are both at least n pixels and adjust both dimensions.
if (Height < 16)
{
Height += 16 - Height;
Width += 16 - Height;
}
if (Width < 16)
{
Height += 16 - Width;
Width += 16 - Width;
}
if (emoji.MimeType?.ToLower() == "image/gif")
{
var resized = ResizeGifImage(decompressedImageBytes, Width, Height);
var itemCache = new ImageCacheItem(resized, "image/gif");
WikiCache.Put(scaledImageCacheKey, itemCache);
return File(itemCache.Bytes, itemCache.ContentType);
}
else
{
using var image = Images.ResizeImage(img, Width, Height);
using var ms = new MemoryStream();
image.SaveAsPng(ms);
var itemCache = new ImageCacheItem(ms.ToArray(), "image/png");
WikiCache.Put(scaledImageCacheKey, itemCache);
return File(itemCache.Bytes, itemCache.ContentType);
}
}
}
}
return NotFound($"Emoji {pageNavigation} 无法找到");
}
}
}