flower-story/Server/Controller/BaseController.cs
2024-10-12 14:34:11 +08:00

446 lines
14 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Blahblah.FlowerStory.Server.Data;
using Blahblah.FlowerStory.Server.Data.Model;
using Microsoft.AspNetCore.Mvc;
using SkiaSharp;
using System.Net;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
namespace Blahblah.FlowerStory.Server.Controller;
/// <summary>
/// 基础服务抽象类
/// </summary>
/// <remarks>
/// 构造基础服务类
/// </remarks>
/// <param name="database">数据库对象</param>
/// <param name="logger">日志对象</param>
public abstract partial class BaseController<T>(FlowerDatabase database, ILogger<T>? logger = null) : ControllerBase
{
private const string Salt = "Blah blah, o! Flower story, intimately help you record every bit of the garden.";
private const int ThumbWidth = 600;
/// <summary>
/// 临时 id
/// </summary>
protected const int TempId = -1;
private static string? uploadsDirectory;
/// <summary>
/// 文件上传路径
/// </summary>
protected static string UploadsDirectory => uploadsDirectory ??= Path.Combine(Program.DataPath, "uploads");
/// <summary>
/// 支持的图片文件签名
/// </summary>
protected static readonly List<byte[]> PhotoSignatures =
[
// jpeg
[0xFF, 0xD8, 0xFF, 0xDB],
[0xFF, 0xD8, 0xFF, 0xE0],
[0xFF, 0xD8, 0xFF, 0xE1],
[0xFF, 0xD8, 0xFF, 0xE2],
[0xFF, 0xD8, 0xFF, 0xE3],
// png
[0x89, 0x50, 0x4E, 0x47]
];
/// <summary>
/// 自定义认证头的关键字
/// </summary>
protected const string AuthHeader = "Authorization";
/// <summary>
/// 禁用用户
/// </summary>
protected const int UserDisabled = -1;
/// <summary>
/// 普通用户
/// </summary>
protected const int UserCommon = 0;
/// <summary>
/// 管理员用户
/// </summary>
protected const int UserAdmin = 99;
/// <summary>
/// 数据库对象
/// </summary>
protected readonly FlowerDatabase database = database;
/// <summary>
/// 日志对象
/// </summary>
protected readonly ILogger? logger = logger;
/// <summary>
/// 计算密码的 hash
/// </summary>
/// <param name="password">密码原文</param>
/// <param name="id">用户 id</param>
/// <returns>密码 hash值为 SHA256(password+id+salt)</returns>
/// <exception cref="ArgumentNullException"></exception>
public string HashPassword(string password, string id)
{
ArgumentException.ThrowIfNullOrEmpty(password);
ArgumentException.ThrowIfNullOrEmpty(id);
var data = Encoding.UTF8.GetBytes($"{password}{id}{Salt}");
var hash = SHA256.HashData(data);
return Convert.ToHexString(hash);
}
/// <summary>
/// 检出当前会话
/// </summary>
/// <returns>若检查失败,第一个参数返回异常 ActionResult。第二个参数返回会话令牌对象</returns>
protected (ActionResult? Result, TokenItem? Token) CheckToken()
{
if (!Request.Headers.TryGetValue(AuthHeader, out var h))
{
logger?.LogWarning("request with no {auth} header", AuthHeader);
return (Unauthorized(), null);
}
string hash = h.ToString();
var token = database.Tokens.Find(hash);
if (token == null)
{
logger?.LogWarning("token \"{hash}\" not found", hash);
return (Unauthorized(), null);
}
return (null, token);
}
/// <summary>
/// 检查当前会话权限
/// </summary>
/// <param name="level">需要大于等于该权限,默认为 0 - UserCommon</param>
/// <returns>若检查失败,第一个参数返回异常 ActionResult。第二个参数返回会话对应的用户对象</returns>
protected (ActionResult? Result, UserItem? User) CheckPermission(int? level = 0)
{
var (result, token) = CheckToken();
if (result != null)
{
return (result, null);
}
if (token == null)
{
return (Unauthorized(), null);
}
if (token.ExpireDate < DateTimeOffset.UtcNow)
{
logger?.LogWarning("token \"{hash}\" has expired after {date}", token.Id, token.ExpireDate);
return (Unauthorized(), null);
}
var user = QueryUserItem(token.UserId);
//var user = database.Users.Where(u => u.Id == token.UserId).Select(u => new UserItem
//{
// Id = u.Id,
// UserId = u.UserId,
// Level = u.Level,
// RegisterDateUnixTime = u.RegisterDateUnixTime,
// ActiveDateUnixTime = u.ActiveDateUnixTime,
// Name = u.Name,
// Email = u.Email,
// Mobile = u.Mobile
//}).SingleOrDefault();
if (user == null)
{
logger?.LogWarning("user not found with id {id}", token.UserId);
}
else if (user.Level < level)
{
logger?.LogWarning("user \"{id}\" level ({level}) lower than required ({required})", user.UserId, user.Level, level);
return (Forbid(), user);
}
else
{
var now = DateTimeOffset.UtcNow;
token.ActiveDateUnixTime = now.ToUnixTimeMilliseconds();
var expires = now.AddSeconds(token.ExpireSeconds).ToUnixTimeMilliseconds();
if (expires > token.ExpireDateUnixTime)
{
token.ExpireDateUnixTime = expires;
}
user.ActiveDateUnixTime = now.ToUnixTimeMilliseconds();
}
return (null, user);
}
/// <summary>
/// 保存数据库变动并输出日志
/// </summary>
/// <returns>操作的数据库行数</returns>
protected int SaveDatabase()
{
var count = database.SaveChanges();
if (count > 0)
{
logger?.LogInformation("{number} of entries written to database.", count);
}
else
{
logger?.LogWarning("no data written to database.");
}
return count;
}
/// <summary>
/// 获取嵌入流的数据
/// </summary>
/// <param name="filename">文件名</param>
/// <param name="namespace">命名空间,默认为程序集.wwwroot</param>
/// <returns></returns>
protected static byte[] GetEmbeddedData(string filename, string? @namespace = null)
{
Assembly asm = Assembly.GetExecutingAssembly();
if (string.IsNullOrEmpty(@namespace))
{
@namespace = typeof(Program).Namespace + ".wwwroot";
}
using Stream? stream = asm.GetManifestResourceStream($"{@namespace}.{filename}");
if (stream == null)
{
return [];
}
using var ms = new MemoryStream();
stream.CopyTo(ms);
return ms.ToArray();
}
/// <summary>
/// 读取文件到 byte 数组
/// </summary>
/// <param name="file">来自请求的文件</param>
/// <returns>文件结果对象</returns>
protected static FileResult? WrapFormFile(IFormFile file)
{
if (file?.Length > 0)
{
using var stream = file.OpenReadStream();
// check header
var headers = new byte[PhotoSignatures.Max(s => s.Length)];
int count = stream.Read(headers, 0, headers.Length);
if (PhotoSignatures.Any(s => headers.Take(s.Length).SequenceEqual(s)))
{
using var ms = new MemoryStream();
ms.Write(headers, 0, count);
#if __OLD_READER__
// reading
const int size = 16384;
var buffer = new byte[size];
while ((count = stream.Read(buffer, 0, size)) > 0)
{
ms.Write(buffer, 0, count);
}
#else
stream.CopyTo(ms);
#endif
var data = ms.ToArray();
var name = file.FileName;
var ext = Path.GetExtension(name);
var path = $"{WebUtility.UrlEncode(Path.GetFileNameWithoutExtension(name))}_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}{ext}";
var image = SKImage.FromEncodedData(data);
return new FileResult
{
Filename = name,
FileType = ext,
Path = path,
Content = data,
Width = image.Width,
Height = image.Height
};
}
}
return null;
}
private static string GetFlowerDirectory(int fid, bool create = false)
{
var directory = Path.Combine(UploadsDirectory, fid.ToString());
if (create && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
return directory;
}
/// <summary>
/// 创建缩略图
/// </summary>
/// <param name="data">图片数据</param>
/// <param name="maxWidth">最大宽度</param>
/// <param name="quality">缩放质量</param>
/// <returns></returns>
protected static byte[] CreateThumbnail(byte[] data, int maxWidth = ThumbWidth, SKFilterQuality quality = SKFilterQuality.Medium)
{
if (maxWidth <= 0)
{
return data;
}
using var image = SKImage.FromEncodedData(data);
if (maxWidth >= image.Width)
{
using var enc = image.Encode(SKEncodedImageFormat.Jpeg, 80);
return enc.ToArray();
}
var height = maxWidth * image.Height / image.Width;
using var bitmap = SKBitmap.FromImage(image);
using var resized = bitmap.Resize(new SKImageInfo(maxWidth, height), quality);
using var encode = resized.Encode(SKEncodedImageFormat.Jpeg, 80);
return encode.ToArray();
}
/// <summary>
/// 写入文件到用户的花草目录中
/// </summary>
/// <param name="fid">花草唯一 id</param>
/// <param name="file">文件对象</param>
/// <param name="thumbnail">创建缩略图</param>
/// <param name="token">取消令牌</param>
protected async Task WriteToFile(int fid, FileResult file, bool thumbnail = true, CancellationToken token = default)
{
var directory = GetFlowerDirectory(fid, true);
var path = Path.Combine(directory, file.Path);
await System.IO.File.WriteAllBytesAsync(path, file.Content, token);
if (thumbnail)
{
try
{
var thumb = CreateThumbnail(file.Content);
path = Path.Combine(directory, $"{file.Path}.thumb");
await System.IO.File.WriteAllBytesAsync(path, thumb, token);
}
catch (Exception ex)
{
logger?.LogError(ex, "failed to create thumbnail for flower: {fid}, file: {file}, error: {message}", fid, file.Filename, ex.Message);
}
}
}
/// <summary>
/// 删除花草下的文件
/// </summary>
/// <param name="fid">花草唯一 id</param>
/// <param name="path">文件路径</param>
/// <returns>返回是否已删除</returns>
protected static bool DeleteFile(int fid, string path)
{
var directory = GetFlowerDirectory(fid);
if (Directory.Exists(directory))
{
path = Path.Combine(directory, path);
if (System.IO.File.Exists(path))
{
System.IO.File.Delete(path);
return true;
}
}
return false;
}
/// <summary>
/// 移动临时照片到花草目录
/// </summary>
/// <param name="fid">花草唯一 id</param>
/// <param name="path">文件路径</param>
protected static void MoveTempFileToFlower(int fid, string path)
{
var directory = GetFlowerDirectory(TempId);
if (Directory.Exists(directory))
{
var file = Path.Combine(directory, path);
if (System.IO.File.Exists(file))
{
directory = GetFlowerDirectory(fid, true);
var to = Path.Combine(directory, path);
if (System.IO.File.Exists(to))
{
System.IO.File.Move(to, $"{to}.bak");
}
System.IO.File.Move(file, to);
}
}
}
private const double EarthRadius = 6378137;
private static double Radius(double degree)
{
return degree * Math.PI / 180.0;
}
/// <summary>
/// 获取两个经纬度之间的距离
/// </summary>
/// <param name="lat1">纬度1</param>
/// <param name="lon1">经度1</param>
/// <param name="lat2">纬度2</param>
/// <param name="lon2">经度2</param>
/// <returns></returns>
protected static double GetDistance(double lat1, double lon1, double lat2, double lon2)
{
double rlat1 = Radius(lat1);
double rlat2 = Radius(lat2);
return EarthRadius * Math.Acos(
Math.Cos(rlat1) * Math.Cos(rlat2) * Math.Cos(Radius(lon1) - Radius(lon2)) +
Math.Sin(rlat1) * Math.Sin(rlat2));
}
/// <inheritdoc/>
protected static double GetDistance2(double lat1, double lon1, double lat2, double lon2)
{
double rlat1 = Radius(lat1);
double rlat2 = Radius(lat2);
double a = rlat1 - rlat2;
double b = Radius(lon1) - Radius(lon2);
double s = 2 * Math.Asin(Math.Sqrt(Math.Pow(Math.Sin(a / 2), 2) + Math.Cos(rlat1) * Math.Cos(rlat2) * Math.Pow(Math.Sin(b / 2), 2)));
return s * EarthRadius;
}
}
/// <summary>
/// 文件结果
/// </summary>
public record FileResult
{
/// <summary>
/// 文件名
/// </summary>
public required string Filename { get; init; }
/// <summary>
/// 文件类型
/// </summary>
public required string FileType { get; init; }
/// <summary>
/// 储存路径
/// </summary>
public required string Path { get; init; }
/// <summary>
/// 文件内容
/// </summary>
public required byte[] Content { get; init; }
/// <summary>
/// 照片宽度
/// </summary>
public required int Width { get; init; }
/// <summary>
/// 照片高度
/// </summary>
public required int Height { get; init; }
}