diff --git a/.gitignore b/.gitignore index 62b24f7..72d2a79 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ ## ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore -flower.db +flower.db* TestCase/ # User-specific files diff --git a/Server/Controller/BaseController.cs b/Server/Controller/BaseController.cs index bcf696d..7c1e268 100644 --- a/Server/Controller/BaseController.cs +++ b/Server/Controller/BaseController.cs @@ -239,7 +239,8 @@ public abstract partial class BaseController : ControllerBase /// <param name="uid">用户唯一 id</param> /// <param name="fid">花草唯一 id</param> /// <param name="file">文件对象</param> - protected async Task WriteToFile(int uid, int fid, FileResult file) + /// <param name="token">取消令牌</param> + protected async Task WriteToFile(int uid, int fid, FileResult file, CancellationToken token = default) { var directory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "uploads", uid.ToString(), fid.ToString()); if (!Directory.Exists(directory)) @@ -247,7 +248,7 @@ public abstract partial class BaseController : ControllerBase Directory.CreateDirectory(directory); } var path = Path.Combine(directory, file.Path); - await System.IO.File.WriteAllBytesAsync(path, file.Content); + await System.IO.File.WriteAllBytesAsync(path, file.Content, token); } } diff --git a/Server/Controller/BaseController.sqlite.cs b/Server/Controller/BaseController.sqlite.cs index 1c6e257..abdb3b5 100644 --- a/Server/Controller/BaseController.sqlite.cs +++ b/Server/Controller/BaseController.sqlite.cs @@ -1,11 +1,31 @@ using Blahblah.FlowerStory.Server.Data.Model; using Microsoft.EntityFrameworkCore; -using System.Security.Cryptography; namespace Blahblah.FlowerStory.Server.Controller; partial class BaseController { + /// <summary> + /// 执行事务 + /// </summary> + /// <param name="executor">执行代理</param> + /// <param name="token">取消令牌</param> + /// <exception cref="ArgumentNullException">执行代理为 null</exception> + protected async Task ExecuteTransaction(Func<CancellationToken, Task> executor, CancellationToken token = default) + { + if (executor == null) throw new ArgumentNullException(nameof(executor)); + using var trans = await database.Database.BeginTransactionAsync(token); + try + { + await executor(token); + await trans.CommitAsync(token); + } + catch + { + await trans.RollbackAsync(token); + throw; + } + } /// <summary> /// 根据 uid 获取用户对象 @@ -50,4 +70,14 @@ partial class BaseController { return database.Database.ExecuteSql($"UPDATE \"users\" SET \"avatar\" = NULL WHERE \"uid\" = {uid}"); } + + /// <summary> + /// 添加照片项 + /// </summary> + /// <param name="item">照片对象</param> + /// <returns></returns> + protected int AddPhotoItem(PhotoItem item) + { + return database.Database.ExecuteSql($"INSERT INTO \"photos\"(\"fid\",\"rid\",\"filetype\",\"filename\",\"path\",\"dateupload\") VALUES({item.FlowerId},{item.RecordId},{item.FileType},{item.FileName},{item.Path},{item.DateUploadUnixTime})"); + } } diff --git a/Server/Controller/EventApiController.cs b/Server/Controller/EventApiController.cs index d4e546c..64a14bd 100644 --- a/Server/Controller/EventApiController.cs +++ b/Server/Controller/EventApiController.cs @@ -95,10 +95,7 @@ public class EventApiController : BaseController if (includePhoto == true) { - foreach (var r in records) - { - r.Photos = database.Photos.Where(p => p.RecordId == r.Id).ToList(); - } + records = records.Include(r => r.Photos); } return Ok(records.ToArray()); @@ -334,9 +331,9 @@ public class EventApiController : BaseController /// </remarks> /// <param name="id">事件唯一 id</param> /// <param name="photo">图片</param> - /// <returns>修改成功则返回 HTTP 204</returns> - /// <response code="204">修改成功</response> - /// <response code="400">照片格式非法</response> + /// <returns>添加成功则返回 HTTP 204</returns> + /// <response code="204">添加成功</response> + /// <response code="400">图片格式非法</response> /// <response code="401">未找到登录会话或已过期</response> /// <response code="403">用户已禁用</response> /// <response code="404">未找到关联用户或者关联的事件</response> @@ -351,7 +348,7 @@ public class EventApiController : BaseController [HttpPost] [Consumes("multipart/form-data")] [RequestSizeLimit(15 * 1024 * 1024)] - public async Task<ActionResult> UploadCovers([Required][FromQuery] int id, [Required] IFormFile photo) + public async Task<ActionResult> UploadPhoto([Required][FromQuery] int id, [Required] IFormFile photo) { var (result, user) = CheckPermission(); if (result != null) @@ -391,7 +388,7 @@ public class EventApiController : BaseController try { - await WriteToFile(user.Id, id, file); + await WriteToFile(user.Id, record.FlowerId, file); } catch (Exception ex) { @@ -405,6 +402,107 @@ public class EventApiController : BaseController return NoContent(); } + /// <summary> + /// 批量添加事件关联照片,总大小限制 75MB + /// </summary> + /// <remarks> + /// 请求示例: + /// + /// POST /api/event/add_photos + /// Authorization: authorization id + /// + /// 参数: + /// + /// id: int + /// photos: IFormFile[] + /// + /// </remarks> + /// <param name="id">事件唯一 id</param> + /// <param name="photos">图片集</param> + /// <returns>添加成功则返回 HTTP 204</returns> + /// <response code="204">添加成功</response> + /// <response code="400">图片参数非法或图片格式非法</response> + /// <response code="401">未找到登录会话或已过期</response> + /// <response code="403">用户已禁用</response> + /// <response code="404">未找到关联用户或者关联的事件</response> + /// <response code="413">提交正文过大</response> + [Route("add_photos", Name = "addEventPhotos")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status413PayloadTooLarge)] + [HttpPost] + [Consumes("multipart/form-data")] + // 5 photos + [RequestSizeLimit(75 * 1024 * 1024)] + public async Task<ActionResult> UploadPhotos([Required][FromQuery] int id, [Required] IFormFile[] photos) + { + var (result, user) = CheckPermission(); + if (result != null) + { + return result; + } + if (user == null) + { + return NotFound(); + } + + if (photos == null || photos.Length == 0) + { + SaveDatabase(); + return BadRequest(); + } + + var record = database.Records.SingleOrDefault(r => r.Id == id && r.OwnerId == user.Id); + if (record == null) + { + SaveDatabase(); + return NotFound(id); + } + + SaveDatabase(); + + try + { + await ExecuteTransaction(async token => + { + foreach (var photo in photos) + { + if (photo.Length > 0) + { + var file = WrapFormFile(photo) ?? throw new BadHttpRequestException(photo?.FileName ?? string.Empty); + + var p = new PhotoItem + { + FlowerId = record.FlowerId, + RecordId = id, + FileType = file.FileType, + FileName = file.Filename, + Path = file.Path, + DateUploadUnixTime = user.ActiveDateUnixTime ?? DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() + }; + AddPhotoItem(p); + + await WriteToFile(user.Id, record.FlowerId, file, token); + } + } + }); + } + catch (BadHttpRequestException bex) + { + return BadRequest(bex.Message); + } + catch (Exception ex) + { + return Problem(ex.ToString(), "api/event/add_photos"); + // TODO: Logger + } + + return NoContent(); + } + /// <summary> /// 获取事件关联的照片列表 /// </summary>