From 4ea535d93753826ec900879560d876cec4d58c38 Mon Sep 17 00:00:00 2001 From: crupest Date: Wed, 10 Feb 2021 02:03:06 +0800 Subject: ... --- BackEnd/Timeline/Services/TimelinePostService.cs | 126 ++++++++--------------- 1 file changed, 45 insertions(+), 81 deletions(-) (limited to 'BackEnd/Timeline/Services/TimelinePostService.cs') diff --git a/BackEnd/Timeline/Services/TimelinePostService.cs b/BackEnd/Timeline/Services/TimelinePostService.cs index 66ec8090..98841478 100644 --- a/BackEnd/Timeline/Services/TimelinePostService.cs +++ b/BackEnd/Timeline/Services/TimelinePostService.cs @@ -14,95 +14,69 @@ using static Timeline.Resources.Services.TimelineService; namespace Timeline.Services { - public class PostData : ICacheableData + public class TimelinePostDataDigest { -#pragma warning disable CA1819 // Properties should not return arrays - public byte[] Data { get; set; } = default!; -#pragma warning restore CA1819 // Properties should not return arrays - public string Type { get; set; } = default!; - public string ETag { get; set; } = default!; - public DateTime? LastModified { get; set; } // TODO: Why nullable? - } + public TimelinePostDataDigest(string kind, string eTag, DateTime lastModified) + { + Kind = kind; + ETag = eTag; + LastModified = lastModified; + } - public abstract class TimelinePostCreateRequestContent - { - public abstract string TypeName { get; } + public string Kind { get; set; } + public string ETag { get; set; } + public DateTime LastModified { get; set; } } - public class TimelinePostCreateRequestTextContent : TimelinePostCreateRequestContent + public class TimelinePostData { - private string _text; - - public TimelinePostCreateRequestTextContent(string text) + public TimelinePostData(string kind, byte[] data, string eTag, DateTime lastModified) { - if (text is null) - throw new ArgumentNullException(nameof(text)); - - _text = text; + Kind = kind; + Data = data; + ETag = eTag; + LastModified = lastModified; } - public override string TypeName => TimelinePostContentTypes.Text; + public string Kind { get; set; } - public string Text - { - get => _text; - set - { - if (value is null) - throw new ArgumentNullException(nameof(value)); - _text = value; - } - } +#pragma warning disable CA1819 // Properties should not return arrays + public byte[] Data { get; set; } +#pragma warning restore CA1819 // Properties should not return arrays + + public string ETag { get; set; } + public DateTime LastModified { get; set; } } - public class TimelinePostCreateRequestImageContent : TimelinePostCreateRequestContent + public class TimelinePostCreateRequestData { - private byte[] _data; - - public TimelinePostCreateRequestImageContent(byte[] data) + public TimelinePostCreateRequestData(string kind, byte[] data) { - if (data is null) - throw new ArgumentNullException(nameof(data)); - - _data = data; + Kind = kind; + Data = data; } - public override string TypeName => TimelinePostContentTypes.Image; - + public string Kind { get; set; } #pragma warning disable CA1819 // Properties should not return arrays - public byte[] Data - { - get => _data; - set - { - if (value is null) - throw new ArgumentNullException(nameof(value)); - _data = value; - } - } + public byte[] Data { get; set; } #pragma warning restore CA1819 // Properties should not return arrays } public class TimelinePostCreateRequest { - public TimelinePostCreateRequest(TimelinePostCreateRequestContent content) - { - Content = content; - } - public string? Color { get; set; } /// If not set, current time is used. public DateTime? Time { get; set; } - public TimelinePostCreateRequestContent Content { get; set; } + public List Content { get; set; } = new List(); } public class TimelinePostPatchRequest { public string? Color { get; set; } public DateTime? Time { get; set; } - public TimelinePostCreateRequestContent? Content { get; set; } + public List? Content { get; set; } } public interface ITimelinePostService @@ -128,16 +102,7 @@ namespace Timeline.Services /// Thrown when post of does not exist or has been deleted. Task GetPost(long timelineId, long postId, bool includeDelete = false); - /// - /// Get the etag of data of a post. - /// - /// The id of the timeline of the post. - /// The id of the post. - /// The etag of the data. - /// Thrown when timeline does not exist. - /// Thrown when post of does not exist or has been deleted. - /// Thrown when post has no data. - Task GetPostDataETag(long timelineId, long postId); + Task GetPostDataDigest(long timelineId, long postId, long dataIndex); /// /// Get the data of a post. @@ -148,8 +113,7 @@ namespace Timeline.Services /// Thrown when timeline does not exist. /// Thrown when post of does not exist or has been deleted. /// Thrown when post has no data. - /// - Task GetPostData(long timelineId, long postId); + Task GetPostData(long timelineId, long postId, long dataIndex); /// /// Create a new post in timeline. @@ -305,7 +269,7 @@ namespace Timeline.Services if (postEntity.Content == null) throw new TimelinePostNotExistException(timelineId, postId, true); - if (postEntity.ContentType != TimelinePostContentTypes.Image) + if (postEntity.ContentType != TimelinePostDataKind.Image) throw new TimelinePostNoDataException(ExceptionGetDataNonImagePost); var tag = postEntity.Content; @@ -313,7 +277,7 @@ namespace Timeline.Services return tag; } - public async Task GetPostData(long timelineId, long postId) + public async Task GetPostData(long timelineId, long postId) { await CheckTimelineExistence(timelineId); @@ -325,7 +289,7 @@ namespace Timeline.Services if (postEntity.Content == null) throw new TimelinePostNotExistException(timelineId, postId, true); - if (postEntity.ContentType != TimelinePostContentTypes.Image) + if (postEntity.ContentType != TimelinePostDataKind.Image) throw new TimelinePostNoDataException(ExceptionGetDataNonImagePost); var tag = postEntity.Content; @@ -349,7 +313,7 @@ namespace Timeline.Services await _database.SaveChangesAsync(); } - return new PostData + return new TimelinePostData { Data = data, Type = postEntity.ExtraContent, @@ -358,21 +322,21 @@ namespace Timeline.Services }; } - private async Task SaveContent(TimelinePostEntity entity, TimelinePostCreateRequestContent content) + private async Task SaveContent(TimelinePostEntity entity, TimelinePostCreateRequestData content) { switch (content) { - case TimelinePostCreateRequestTextContent c: - entity.ContentType = c.TypeName; - entity.Content = c.Text; + case TimelinePostCreateRequestTextData c: + entity.ContentType = c.Kind; + entity.Content = c.Data; break; - case TimelinePostCreateRequestImageContent c: + case TimelinePostCreateRequestImageData c: var imageFormat = await _imageValidator.Validate(c.Data); var imageFormatText = imageFormat.DefaultMimeType; var tag = await _dataManager.RetainEntry(c.Data); - entity.ContentType = content.TypeName; + entity.ContentType = content.Kind; entity.Content = tag; entity.ExtraContent = imageFormatText; break; @@ -383,7 +347,7 @@ namespace Timeline.Services private async Task CleanContent(TimelinePostEntity entity) { - if (entity.Content is not null && entity.ContentType == TimelinePostContentTypes.Image) + if (entity.Content is not null && entity.ContentType == TimelinePostDataKind.Image) await _dataManager.FreeEntry(entity.Content); entity.Content = null; } @@ -516,7 +480,7 @@ namespace Timeline.Services { if (post.Content != null) { - if (post.ContentType == TimelinePostContentTypes.Image) + if (post.ContentType == TimelinePostDataKind.Image) { dataTags.Add(post.Content); } -- cgit v1.2.3 From 98df0fb9fe94b703325f09bf765904a11e8a7d4c Mon Sep 17 00:00:00 2001 From: crupest Date: Wed, 10 Feb 2021 14:31:31 +0800 Subject: ... --- BackEnd/Timeline.ErrorCodes/ErrorCodes.cs | 2 +- .../Timeline/Controllers/TimelinePostController.cs | 158 ++++++++------------- .../CatchTimelineNotExistExceptionAttribute.cs | 32 +++++ ...chTimelinePostDataNotExistExceptionAttribute.cs | 24 ++++ .../CatchTimelinePostNotExistExceptionAttribute.cs | 24 ++++ BackEnd/Timeline/Filters/Timeline.cs | 32 ----- .../Timeline/Formatters/ByteDataInputFormatter.cs | 80 +++++++++++ BackEnd/Timeline/Formatters/BytesInputFormatter.cs | 79 ----------- .../Migrations/20200312112552_AddImagePost.cs | 2 +- BackEnd/Timeline/Models/Http/ErrorResponse.cs | 11 -- .../Models/Http/HttpTimelinePostCreateRequest.cs | 17 +-- .../Http/HttpTimelinePostCreateRequestData.cs | 19 +++ BackEnd/Timeline/Models/MimeTypes.cs | 14 ++ .../Timeline/Models/TimelinePostContentTypes.cs | 13 -- .../Validation/TimelinePostContentTypeValidator.cs | 19 --- .../Exceptions/TimelinePostNoDataException.cs | 15 -- .../Services/TimelinePostCreateDataException.cs | 16 +++ .../Services/TimelinePostDataNotExistException.cs | 15 ++ BackEnd/Timeline/Services/TimelinePostService.cs | 68 +++------ BackEnd/Timeline/Startup.cs | 2 +- 20 files changed, 308 insertions(+), 334 deletions(-) create mode 100644 BackEnd/Timeline/Filters/CatchTimelineNotExistExceptionAttribute.cs create mode 100644 BackEnd/Timeline/Filters/CatchTimelinePostDataNotExistExceptionAttribute.cs create mode 100644 BackEnd/Timeline/Filters/CatchTimelinePostNotExistExceptionAttribute.cs delete mode 100644 BackEnd/Timeline/Filters/Timeline.cs create mode 100644 BackEnd/Timeline/Formatters/ByteDataInputFormatter.cs delete mode 100644 BackEnd/Timeline/Formatters/BytesInputFormatter.cs create mode 100644 BackEnd/Timeline/Models/Http/HttpTimelinePostCreateRequestData.cs create mode 100644 BackEnd/Timeline/Models/MimeTypes.cs delete mode 100644 BackEnd/Timeline/Models/TimelinePostContentTypes.cs delete mode 100644 BackEnd/Timeline/Models/Validation/TimelinePostContentTypeValidator.cs delete mode 100644 BackEnd/Timeline/Services/Exceptions/TimelinePostNoDataException.cs create mode 100644 BackEnd/Timeline/Services/TimelinePostCreateDataException.cs create mode 100644 BackEnd/Timeline/Services/TimelinePostDataNotExistException.cs (limited to 'BackEnd/Timeline/Services/TimelinePostService.cs') diff --git a/BackEnd/Timeline.ErrorCodes/ErrorCodes.cs b/BackEnd/Timeline.ErrorCodes/ErrorCodes.cs index 8211a0cc..4c3b6cd8 100644 --- a/BackEnd/Timeline.ErrorCodes/ErrorCodes.cs +++ b/BackEnd/Timeline.ErrorCodes/ErrorCodes.cs @@ -61,7 +61,7 @@ public const int NotExist = 1_104_02_01; public const int QueryRelateNotExist = 1_104_04_01; public const int PostNotExist = 1_104_05_01; - public const int PostNoData = 1_104_05_02; + public const int PostDataNotExist = 1_104_05_02; } public static class HighlightTimelineController diff --git a/BackEnd/Timeline/Controllers/TimelinePostController.cs b/BackEnd/Timeline/Controllers/TimelinePostController.cs index a0fd1687..06082f0f 100644 --- a/BackEnd/Timeline/Controllers/TimelinePostController.cs +++ b/BackEnd/Timeline/Controllers/TimelinePostController.cs @@ -4,14 +4,14 @@ using Microsoft.AspNetCore.Mvc; using System; using System.Collections.Generic; using System.Threading.Tasks; +using System.ComponentModel.DataAnnotations; using Timeline.Filters; -using Timeline.Helpers; +using Timeline.Helpers.Cache; using Timeline.Models; using Timeline.Models.Http; using Timeline.Models.Mapper; using Timeline.Models.Validation; using Timeline.Services; -using Timeline.Services.Exceptions; namespace Timeline.Controllers { @@ -21,6 +21,8 @@ namespace Timeline.Controllers [ApiController] [Route("timelines/{timeline}/posts")] [CatchTimelineNotExistException] + [CatchTimelinePostNotExistException] + [CatchTimelinePostDataNotExistException] [ProducesErrorResponseType(typeof(CommonResponse))] public class TimelinePostController : Controller { @@ -86,57 +88,27 @@ namespace Timeline.Controllers return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); } - try - { - var post = await _postService.GetPost(timelineId, postId); - var result = await _timelineMapper.MapToHttp(post, timeline, Url); - return result; - } - catch (TimelinePostNotExistException) - { - return NotFound(ErrorResponse.TimelineController.PostNotExist()); - } + var post = await _postService.GetPost(timelineId, postId); + var result = await _timelineMapper.MapToHttp(post, timeline, Url); + return result; } /// - /// Get the data of a post. Usually a image post. + /// Get the first data of a post. /// /// Timeline name. /// The id of the post. - /// If-None-Match header. /// The data. [HttpGet("{post}/data")] - [Produces("image/png", "image/jpeg", "image/gif", "image/webp", "application/json", "text/json")] + [Produces(MimeTypes.ImagePng, MimeTypes.ImageJpeg, MimeTypes.ImageGif, MimeTypes.ImageWebp, MimeTypes.TextPlain, MimeTypes.TextMarkdown, MimeTypes.TextPlain, MimeTypes.ApplicationJson)] [ProducesResponseType(typeof(byte[]), StatusCodes.Status200OK)] [ProducesResponseType(typeof(void), StatusCodes.Status304NotModified)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task DataIndexGet([FromRoute][GeneralTimelineName] string timeline, [FromRoute] long post, [FromHeader(Name = "If-None-Match")] string? ifNoneMatch) + public async Task> DataIndexGet([FromRoute][GeneralTimelineName] string timeline, [FromRoute] long post) { - _ = ifNoneMatch; - - var timelineId = await _timelineService.GetTimelineIdByName(timeline); - - if (!UserHasAllTimelineManagementPermission && !await _timelineService.HasReadPermission(timelineId, this.GetOptionalUserId())) - { - return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); - } - - try - { - return await DataCacheHelper.GenerateActionResult(this, - () => _postService.GetPostDataETag(timelineId, post), - async () => await _postService.GetPostData(timelineId, post)); - } - catch (TimelinePostNotExistException) - { - return NotFound(ErrorResponse.TimelineController.PostNotExist()); - } - catch (TimelinePostNoDataException) - { - return BadRequest(ErrorResponse.TimelineController.PostNoData()); - } + return await DataGet(timeline, post, 0); } /// @@ -144,17 +116,28 @@ namespace Timeline.Controllers /// /// Timeline name. /// The id of the post. - /// If-None-Match header. + /// Index of the data. /// The data. [HttpGet("{post}/data/{data_index}")] - [Produces("image/png", "image/jpeg", "image/gif", "image/webp", "application/json", "text/json")] + [Produces(MimeTypes.ImagePng, MimeTypes.ImageJpeg, MimeTypes.ImageGif, MimeTypes.ImageWebp, MimeTypes.TextPlain, MimeTypes.TextMarkdown, MimeTypes.TextPlain, MimeTypes.ApplicationJson)] [ProducesResponseType(typeof(byte[]), StatusCodes.Status200OK)] [ProducesResponseType(typeof(void), StatusCodes.Status304NotModified)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task DataGet([FromRoute][GeneralTimelineName] string timeline, [FromRoute] long post, [FromHeader(Name = "If-None-Match")] string? ifNoneMatch) + public async Task DataGet([FromRoute][GeneralTimelineName] string timeline, [FromRoute] long post, [FromRoute(Name = "data_index")][Range(0, 100)] long dataIndex) { + var timelineId = await _timelineService.GetTimelineIdByName(timeline); + + if (!UserHasAllTimelineManagementPermission && !await _timelineService.HasReadPermission(timelineId, this.GetOptionalUserId())) + { + return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); + } + + return await DataCacheHelper.GenerateActionResult(this, + () => _postService.GetPostDataDigest(timelineId, post, dataIndex), + () => _postService.GetPostData(timelineId, post, dataIndex) + ); } /// @@ -179,50 +162,36 @@ namespace Timeline.Controllers return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); } - var requestContent = body.Content; - - TimelinePostCreateRequestData createContent; - - switch (requestContent.Type) + var createRequest = new TimelinePostCreateRequest() { - case TimelinePostDataKind.Text: - if (requestContent.Text is null) - { - return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_TextContentTextRequired)); - } - createContent = new TimelinePostCreateRequestTextContent(requestContent.Text); - break; - case TimelinePostDataKind.Image: - if (requestContent.Data is null) - return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ImageContentDataRequired)); - - // decode base64 - byte[] data; - try - { - data = Convert.FromBase64String(requestContent.Data); - } - catch (FormatException) - { - return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ImageContentDataNotBase64)); - } - - createContent = new TimelinePostCreateRequestImageData(data); - break; - default: - return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ContentUnknownType)); + Time = body.Time, + Color = body.Color + }; + for (int i = 0; i < body.DataList.Count; i++) + { + var data = body.DataList[i]; + try + { + var d = Convert.FromBase64String(data.Data); + createRequest.DataList.Add(new TimelinePostCreateRequestData(data.ContentType, d)); + } + catch (FormatException) + { + return BadRequest(new CommonResponse(ErrorCodes.Common.InvalidModel, $"Data at index {i} is not a valid base64 string.")); + } } + try { - var post = await _postService.CreatePost(timelineId, userId, new TimelinePostCreateRequest(createContent) { Time = body.Time, Color = body.Color }); + var post = await _postService.CreatePost(timelineId, userId, createRequest); var result = await _timelineMapper.MapToHttp(post, timeline, Url); return result; } - catch (ImageException) + catch (TimelinePostCreateDataException e) { - return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ImageContentDataNotImage)); + return BadRequest(new CommonResponse(ErrorCodes.Common.InvalidModel, $"Data at index {e.Index} is invalid. {e.Message}")); } } @@ -243,21 +212,15 @@ namespace Timeline.Controllers { var timelineId = await _timelineService.GetTimelineIdByName(timeline); - try + if (!UserHasAllTimelineManagementPermission && !await _postService.HasPostModifyPermission(timelineId, post, this.GetUserId(), true)) { - if (!UserHasAllTimelineManagementPermission && !await _postService.HasPostModifyPermission(timelineId, post, this.GetUserId(), true)) - { - return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); - } - - var entity = await _postService.PatchPost(timelineId, post, new TimelinePostPatchRequest { Time = body.Time, Color = body.Color }); - var result = await _timelineMapper.MapToHttp(entity, timeline, Url); - return Ok(result); - } - catch (TimelinePostNotExistException) - { - return BadRequest(ErrorResponse.TimelineController.PostNotExist()); + return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); } + + var entity = await _postService.PatchPost(timelineId, post, new TimelinePostPatchRequest { Time = body.Time, Color = body.Color }); + var result = await _timelineMapper.MapToHttp(entity, timeline, Url); + + return Ok(result); } /// @@ -276,19 +239,14 @@ namespace Timeline.Controllers { var timelineId = await _timelineService.GetTimelineIdByName(timeline); - try + if (!UserHasAllTimelineManagementPermission && !await _postService.HasPostModifyPermission(timelineId, post, this.GetUserId(), true)) { - if (!UserHasAllTimelineManagementPermission && !await _postService.HasPostModifyPermission(timelineId, post, this.GetUserId(), true)) - { - return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); - } - await _postService.DeletePost(timelineId, post); - return Ok(); - } - catch (TimelinePostNotExistException) - { - return BadRequest(ErrorResponse.TimelineController.PostNotExist()); + return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); } + + await _postService.DeletePost(timelineId, post); + + return Ok(); } } } diff --git a/BackEnd/Timeline/Filters/CatchTimelineNotExistExceptionAttribute.cs b/BackEnd/Timeline/Filters/CatchTimelineNotExistExceptionAttribute.cs new file mode 100644 index 00000000..857d1d2b --- /dev/null +++ b/BackEnd/Timeline/Filters/CatchTimelineNotExistExceptionAttribute.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Timeline.Models.Http; +using Timeline.Services.Exceptions; + +namespace Timeline.Filters +{ + public class CatchTimelineNotExistExceptionAttribute : ExceptionFilterAttribute + { + public override void OnException(ExceptionContext context) + { + if (context.Exception is TimelineNotExistException e) + { + if (e.InnerException is UserNotExistException) + { + if (HttpMethods.IsGet(context.HttpContext.Request.Method)) + context.Result = new NotFoundObjectResult(ErrorResponse.UserCommon.NotExist()); + else + context.Result = new BadRequestObjectResult(ErrorResponse.UserCommon.NotExist()); + } + else + { + if (HttpMethods.IsGet(context.HttpContext.Request.Method)) + context.Result = new NotFoundObjectResult(ErrorResponse.TimelineController.NotExist()); + else + context.Result = new BadRequestObjectResult(ErrorResponse.TimelineController.NotExist()); + } + } + } + } +} diff --git a/BackEnd/Timeline/Filters/CatchTimelinePostDataNotExistExceptionAttribute.cs b/BackEnd/Timeline/Filters/CatchTimelinePostDataNotExistExceptionAttribute.cs new file mode 100644 index 00000000..8b5868aa --- /dev/null +++ b/BackEnd/Timeline/Filters/CatchTimelinePostDataNotExistExceptionAttribute.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Timeline.Models.Http; +using Timeline.Services; + +namespace Timeline.Filters +{ + public class CatchTimelinePostDataNotExistExceptionAttribute : ExceptionFilterAttribute + { + public override void OnException(ExceptionContext context) + { + const string message = "Timeline post data does not exist."; + + if (context.Exception is TimelinePostDataNotExistException e) + { + if (HttpMethods.IsGet(context.HttpContext.Request.Method)) + context.Result = new NotFoundObjectResult(new CommonResponse(ErrorCodes.TimelineController.PostNotExist, message)); + else + context.Result = new BadRequestObjectResult(new CommonResponse(ErrorCodes.TimelineController.PostNotExist, message)); + } + } + } +} diff --git a/BackEnd/Timeline/Filters/CatchTimelinePostNotExistExceptionAttribute.cs b/BackEnd/Timeline/Filters/CatchTimelinePostNotExistExceptionAttribute.cs new file mode 100644 index 00000000..ac3789c7 --- /dev/null +++ b/BackEnd/Timeline/Filters/CatchTimelinePostNotExistExceptionAttribute.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Timeline.Models.Http; +using Timeline.Services.Exceptions; + +namespace Timeline.Filters +{ + public class CatchTimelinePostNotExistExceptionAttribute : ExceptionFilterAttribute + { + public override void OnException(ExceptionContext context) + { + const string message = "Timeline post does not exist."; + + if (context.Exception is TimelinePostNotExistException e) + { + if (HttpMethods.IsGet(context.HttpContext.Request.Method)) + context.Result = new NotFoundObjectResult(new CommonResponse(ErrorCodes.TimelineController.PostNotExist, message)); + else + context.Result = new BadRequestObjectResult(new CommonResponse(ErrorCodes.TimelineController.PostNotExist, message)); + } + } + } +} diff --git a/BackEnd/Timeline/Filters/Timeline.cs b/BackEnd/Timeline/Filters/Timeline.cs deleted file mode 100644 index 6a730ee7..00000000 --- a/BackEnd/Timeline/Filters/Timeline.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; -using Timeline.Models.Http; -using Timeline.Services.Exceptions; - -namespace Timeline.Filters -{ - public class CatchTimelineNotExistExceptionAttribute : ExceptionFilterAttribute - { - public override void OnException(ExceptionContext context) - { - if (context.Exception is TimelineNotExistException e) - { - if (e.InnerException is UserNotExistException) - { - if (HttpMethods.IsGet(context.HttpContext.Request.Method)) - context.Result = new NotFoundObjectResult(ErrorResponse.UserCommon.NotExist()); - else - context.Result = new BadRequestObjectResult(ErrorResponse.UserCommon.NotExist()); - } - else - { - if (HttpMethods.IsGet(context.HttpContext.Request.Method)) - context.Result = new NotFoundObjectResult(ErrorResponse.TimelineController.NotExist()); - else - context.Result = new BadRequestObjectResult(ErrorResponse.TimelineController.NotExist()); - } - } - } - } -} diff --git a/BackEnd/Timeline/Formatters/ByteDataInputFormatter.cs b/BackEnd/Timeline/Formatters/ByteDataInputFormatter.cs new file mode 100644 index 00000000..2451ead6 --- /dev/null +++ b/BackEnd/Timeline/Formatters/ByteDataInputFormatter.cs @@ -0,0 +1,80 @@ +using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System; +using System.Threading.Tasks; +using Timeline.Models; + +namespace Timeline.Formatters +{ + /// + /// Formatter that reads body as byte data. + /// + public class ByteDataInputFormatter : InputFormatter + { + /// + /// + /// + public ByteDataInputFormatter() + { + SupportedMediaTypes.Add(MimeTypes.ImagePng); + SupportedMediaTypes.Add(MimeTypes.ImageJpeg); + SupportedMediaTypes.Add(MimeTypes.ImageGif); + SupportedMediaTypes.Add(MimeTypes.ImageWebp); + SupportedMediaTypes.Add(MimeTypes.TextPlain); + SupportedMediaTypes.Add(MimeTypes.TextMarkdown); + } + + /// + public override bool CanRead(InputFormatterContext context) + { + if (context == null) throw new ArgumentNullException(nameof(context)); + + if (context.ModelType == typeof(ByteData)) + return true; + + return false; + } + + /// + public override async Task ReadRequestBodyAsync(InputFormatterContext context) + { + var request = context.HttpContext.Request; + var contentLength = request.ContentLength; + + var logger = context.HttpContext.RequestServices.GetRequiredService>(); + + if (contentLength == null) + { + logger.LogInformation("Failed to read body as bytes. Content-Length is not set."); + return await InputFormatterResult.FailureAsync(); + } + + if (contentLength == 0) + { + logger.LogInformation("Failed to read body as bytes. Content-Length is 0."); + return await InputFormatterResult.FailureAsync(); + } + + var bodyStream = request.Body; + + var data = new byte[contentLength.Value]; + var bytesRead = await bodyStream.ReadAsync(data); + + if (bytesRead != contentLength) + { + logger.LogInformation("Failed to read body as bytes. Actual length of body is smaller than Content-Length."); + return await InputFormatterResult.FailureAsync(); + } + + var extraByte = new byte[1]; + if (await bodyStream.ReadAsync(extraByte) != 0) + { + logger.LogInformation("Failed to read body as bytes. Actual length of body is greater than Content-Length."); + return await InputFormatterResult.FailureAsync(); + } + + return await InputFormatterResult.SuccessAsync(new ByteData(data, request.ContentType)); + } + } +} diff --git a/BackEnd/Timeline/Formatters/BytesInputFormatter.cs b/BackEnd/Timeline/Formatters/BytesInputFormatter.cs deleted file mode 100644 index ac6537c9..00000000 --- a/BackEnd/Timeline/Formatters/BytesInputFormatter.cs +++ /dev/null @@ -1,79 +0,0 @@ -using Microsoft.AspNetCore.Mvc.Formatters; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Net.Http.Headers; -using System; -using System.Threading.Tasks; -using Timeline.Models; - -namespace Timeline.Formatters -{ - /// - /// Formatter that reads body as bytes. - /// - public class BytesInputFormatter : InputFormatter - { - /// - /// - /// - public BytesInputFormatter() - { - SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/png")); - SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/jpeg")); - SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/gif")); - SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/webp")); - } - - /// - public override bool CanRead(InputFormatterContext context) - { - if (context == null) throw new ArgumentNullException(nameof(context)); - - if (context.ModelType == typeof(ByteData)) - return true; - - return false; - } - - /// - public override async Task ReadRequestBodyAsync(InputFormatterContext context) - { - var request = context.HttpContext.Request; - var contentLength = request.ContentLength; - - var logger = context.HttpContext.RequestServices.GetRequiredService>(); - - if (contentLength == null) - { - logger.LogInformation("Failed to read body as bytes. Content-Length is not set."); - return await InputFormatterResult.FailureAsync(); - } - - if (contentLength == 0) - { - logger.LogInformation("Failed to read body as bytes. Content-Length is 0."); - return await InputFormatterResult.FailureAsync(); - } - - var bodyStream = request.Body; - - var data = new byte[contentLength.Value]; - var bytesRead = await bodyStream.ReadAsync(data); - - if (bytesRead != contentLength) - { - logger.LogInformation("Failed to read body as bytes. Actual length of body is smaller than Content-Length."); - return await InputFormatterResult.FailureAsync(); - } - - var extraByte = new byte[1]; - if (await bodyStream.ReadAsync(extraByte) != 0) - { - logger.LogInformation("Failed to read body as bytes. Actual length of body is greater than Content-Length."); - return await InputFormatterResult.FailureAsync(); - } - - return await InputFormatterResult.SuccessAsync(new ByteData(data, request.ContentType)); - } - } -} diff --git a/BackEnd/Timeline/Migrations/20200312112552_AddImagePost.cs b/BackEnd/Timeline/Migrations/20200312112552_AddImagePost.cs index b6cc29a3..7d9c6614 100644 --- a/BackEnd/Timeline/Migrations/20200312112552_AddImagePost.cs +++ b/BackEnd/Timeline/Migrations/20200312112552_AddImagePost.cs @@ -20,7 +20,7 @@ namespace Timeline.Migrations migrationBuilder.Sql($@" UPDATE timeline_posts -SET content_type = '{TimelinePostDataKind.Text}'; +SET content_type = 'text'; "); } diff --git a/BackEnd/Timeline/Models/Http/ErrorResponse.cs b/BackEnd/Timeline/Models/Http/ErrorResponse.cs index 1bc46680..3812471d 100644 --- a/BackEnd/Timeline/Models/Http/ErrorResponse.cs +++ b/BackEnd/Timeline/Models/Http/ErrorResponse.cs @@ -253,17 +253,6 @@ namespace Timeline.Models.Http { return new CommonResponse(ErrorCodes.TimelineController.PostNotExist, string.Format(message, formatArgs)); } - - public static CommonResponse PostNoData(params object?[] formatArgs) - { - return new CommonResponse(ErrorCodes.TimelineController.PostNoData, string.Format(TimelineController_PostNoData, formatArgs)); - } - - public static CommonResponse CustomMessage_PostNoData(string message, params object?[] formatArgs) - { - return new CommonResponse(ErrorCodes.TimelineController.PostNoData, string.Format(message, formatArgs)); - } - } } diff --git a/BackEnd/Timeline/Models/Http/HttpTimelinePostCreateRequest.cs b/BackEnd/Timeline/Models/Http/HttpTimelinePostCreateRequest.cs index 20d1a25b..07d823ad 100644 --- a/BackEnd/Timeline/Models/Http/HttpTimelinePostCreateRequest.cs +++ b/BackEnd/Timeline/Models/Http/HttpTimelinePostCreateRequest.cs @@ -5,22 +5,6 @@ using Timeline.Models.Validation; namespace Timeline.Models.Http { - public class HttpTimelinePostCreateRequestData - { - /// - /// Kind of the data. - /// - [Required] - [TimelinePostDataKind] - public string Kind { get; set; } = default!; - - /// - /// The true data. If kind is text or markdown, this is a string. If kind is image, this is base64 of data. - /// - [Required] - public string Data { get; set; } = default!; - } - public class HttpTimelinePostCreateRequest { /// @@ -28,6 +12,7 @@ namespace Timeline.Models.Http /// [Required] [MinLength(1)] + [MaxLength(100)] public List DataList { get; set; } = default!; /// diff --git a/BackEnd/Timeline/Models/Http/HttpTimelinePostCreateRequestData.cs b/BackEnd/Timeline/Models/Http/HttpTimelinePostCreateRequestData.cs new file mode 100644 index 00000000..94ee5aa7 --- /dev/null +++ b/BackEnd/Timeline/Models/Http/HttpTimelinePostCreateRequestData.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations; + +namespace Timeline.Models.Http +{ + public class HttpTimelinePostCreateRequestData + { + /// + /// Mime type of the data. + /// + [Required] + public string ContentType { get; set; } = default!; + + /// + /// Base64 of data. + /// + [Required] + public string Data { get; set; } = default!; + } +} diff --git a/BackEnd/Timeline/Models/MimeTypes.cs b/BackEnd/Timeline/Models/MimeTypes.cs new file mode 100644 index 00000000..37d3a893 --- /dev/null +++ b/BackEnd/Timeline/Models/MimeTypes.cs @@ -0,0 +1,14 @@ +namespace Timeline.Models +{ + public static class MimeTypes + { + public const string ImagePng = "image/png"; + public const string ImageJpeg = "image/jpeg"; + public const string ImageGif = "image/gif"; + public const string ImageWebp = "image/webp"; + public const string TextPlain = "text/plain"; + public const string TextMarkdown = "text/markdown"; + public const string TextJson = "text/json"; + public const string ApplicationJson = "application/json"; + } +} diff --git a/BackEnd/Timeline/Models/TimelinePostContentTypes.cs b/BackEnd/Timeline/Models/TimelinePostContentTypes.cs deleted file mode 100644 index d432e03c..00000000 --- a/BackEnd/Timeline/Models/TimelinePostContentTypes.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Collections.Generic; - -namespace Timeline.Models -{ - public static class TimelinePostDataKind - { - public static IReadOnlyList AllTypes { get; } = new List { Text, Image, Markdown }; - - public const string Text = "text"; - public const string Image = "image"; - public const string Markdown = "markdown"; - } -} diff --git a/BackEnd/Timeline/Models/Validation/TimelinePostContentTypeValidator.cs b/BackEnd/Timeline/Models/Validation/TimelinePostContentTypeValidator.cs deleted file mode 100644 index b65c846c..00000000 --- a/BackEnd/Timeline/Models/Validation/TimelinePostContentTypeValidator.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Linq; - -namespace Timeline.Models.Validation -{ - public class TimelinePostDataKindValidator : StringSetValidator - { - public TimelinePostDataKindValidator() : base(TimelinePostDataKind.AllTypes.ToArray()) { } - } - - [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)] - public class TimelinePostDataKindAttribute : ValidateWithAttribute - { - public TimelinePostDataKindAttribute() : base(typeof(TimelinePostDataKindValidator)) - { - - } - } -} diff --git a/BackEnd/Timeline/Services/Exceptions/TimelinePostNoDataException.cs b/BackEnd/Timeline/Services/Exceptions/TimelinePostNoDataException.cs deleted file mode 100644 index c4b6bf62..00000000 --- a/BackEnd/Timeline/Services/Exceptions/TimelinePostNoDataException.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace Timeline.Services.Exceptions -{ - [Serializable] - public class TimelinePostNoDataException : Exception - { - public TimelinePostNoDataException() : this(null, null) { } - public TimelinePostNoDataException(string? message) : this(message, null) { } - public TimelinePostNoDataException(string? message, Exception? inner) : base(Resources.Services.Exceptions.TimelineNoDataException.AppendAdditionalMessage(message), inner) { } - protected TimelinePostNoDataException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - } -} diff --git a/BackEnd/Timeline/Services/TimelinePostCreateDataException.cs b/BackEnd/Timeline/Services/TimelinePostCreateDataException.cs new file mode 100644 index 00000000..fd1e6664 --- /dev/null +++ b/BackEnd/Timeline/Services/TimelinePostCreateDataException.cs @@ -0,0 +1,16 @@ +namespace Timeline.Services +{ + [System.Serializable] + public class TimelinePostCreateDataException : System.Exception + { + public TimelinePostCreateDataException() { } + public TimelinePostCreateDataException(string message) : base(message) { } + public TimelinePostCreateDataException(string message, System.Exception inner) : base(message, inner) { } + public TimelinePostCreateDataException(long index, string? message, System.Exception? inner = null) : base(message, inner) { Index = index; } + protected TimelinePostCreateDataException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + public long Index { get; } + } +} diff --git a/BackEnd/Timeline/Services/TimelinePostDataNotExistException.cs b/BackEnd/Timeline/Services/TimelinePostDataNotExistException.cs new file mode 100644 index 00000000..eac7a771 --- /dev/null +++ b/BackEnd/Timeline/Services/TimelinePostDataNotExistException.cs @@ -0,0 +1,15 @@ +using System; + +namespace Timeline.Services +{ + [Serializable] + public class TimelinePostDataNotExistException : Exception + { + public TimelinePostDataNotExistException() : this(null, null) { } + public TimelinePostDataNotExistException(string? message) : this(message, null) { } + public TimelinePostDataNotExistException(string? message, Exception? inner) : base(message, inner) { } + protected TimelinePostDataNotExistException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + } +} diff --git a/BackEnd/Timeline/Services/TimelinePostService.cs b/BackEnd/Timeline/Services/TimelinePostService.cs index 98841478..cea702a1 100644 --- a/BackEnd/Timeline/Services/TimelinePostService.cs +++ b/BackEnd/Timeline/Services/TimelinePostService.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Threading.Tasks; using Timeline.Entities; using Timeline.Helpers; +using Timeline.Helpers.Cache; using Timeline.Models; using Timeline.Models.Validation; using Timeline.Services.Exceptions; @@ -14,49 +15,15 @@ using static Timeline.Resources.Services.TimelineService; namespace Timeline.Services { - public class TimelinePostDataDigest - { - public TimelinePostDataDigest(string kind, string eTag, DateTime lastModified) - { - Kind = kind; - ETag = eTag; - LastModified = lastModified; - } - - public string Kind { get; set; } - public string ETag { get; set; } - public DateTime LastModified { get; set; } - } - - public class TimelinePostData - { - public TimelinePostData(string kind, byte[] data, string eTag, DateTime lastModified) - { - Kind = kind; - Data = data; - ETag = eTag; - LastModified = lastModified; - } - - public string Kind { get; set; } - -#pragma warning disable CA1819 // Properties should not return arrays - public byte[] Data { get; set; } -#pragma warning restore CA1819 // Properties should not return arrays - - public string ETag { get; set; } - public DateTime LastModified { get; set; } - } - public class TimelinePostCreateRequestData { - public TimelinePostCreateRequestData(string kind, byte[] data) + public TimelinePostCreateRequestData(string contentType, byte[] data) { - Kind = kind; + ContentType = contentType; Data = data; } - public string Kind { get; set; } + public string ContentType { get; set; } #pragma warning disable CA1819 // Properties should not return arrays public byte[] Data { get; set; } #pragma warning restore CA1819 // Properties should not return arrays @@ -69,14 +36,13 @@ namespace Timeline.Services /// If not set, current time is used. public DateTime? Time { get; set; } - public List Content { get; set; } = new List(); + public List DataList { get; set; } = new List(); } public class TimelinePostPatchRequest { public string? Color { get; set; } public DateTime? Time { get; set; } - public List? Content { get; set; } } public interface ITimelinePostService @@ -102,18 +68,29 @@ namespace Timeline.Services /// Thrown when post of does not exist or has been deleted. Task GetPost(long timelineId, long postId, bool includeDelete = false); - Task GetPostDataDigest(long timelineId, long postId, long dataIndex); + /// + /// Get the data digest of a post. + /// + /// The timeline id. + /// The post id. + /// The index of the data. + /// The data digest. + /// Thrown when timeline does not exist. + /// Thrown when post of does not exist or has been deleted. + /// Thrown when data of that index does not exist. + Task GetPostDataDigest(long timelineId, long postId, long dataIndex); /// /// Get the data of a post. /// - /// The id of the timeline of the post. - /// The id of the post. - /// The etag of the data. + /// The timeline id. + /// The post id. + /// The index of the data. + /// The data. /// Thrown when timeline does not exist. /// Thrown when post of does not exist or has been deleted. - /// Thrown when post has no data. - Task GetPostData(long timelineId, long postId, long dataIndex); + /// Thrown when data of that index does not exist. + Task GetPostData(long timelineId, long postId, long dataIndex); /// /// Create a new post in timeline. @@ -140,7 +117,6 @@ namespace Timeline.Services /// Thrown when is of invalid format. /// Thrown when timeline does not exist. /// Thrown when post does not exist. - /// Thrown if data is not a image. Validated by . Task PatchPost(long timelineId, long postId, TimelinePostPatchRequest request); /// diff --git a/BackEnd/Timeline/Startup.cs b/BackEnd/Timeline/Startup.cs index 0fab798b..5951dc80 100644 --- a/BackEnd/Timeline/Startup.cs +++ b/BackEnd/Timeline/Startup.cs @@ -61,7 +61,7 @@ namespace Timeline services.AddControllers(setup => { setup.InputFormatters.Add(new StringInputFormatter()); - setup.InputFormatters.Add(new BytesInputFormatter()); + setup.InputFormatters.Add(new ByteDataInputFormatter()); setup.Filters.Add(new ConsumesAttribute(MediaTypeNames.Application.Json, "text/json")); setup.Filters.Add(new ProducesAttribute(MediaTypeNames.Application.Json, "text/json")); setup.UseApiRoutePrefix("api"); -- cgit v1.2.3 From d1317bd9fe08a933a13df88ba692343cde549123 Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 11 Feb 2021 20:10:56 +0800 Subject: ... --- .../Timeline/Entities/TimelinePostDataEntity.cs | 3 +- BackEnd/Timeline/Services/DataManager.cs | 19 +- .../Services/TimelinePostCreateDataException.cs | 2 +- .../Services/TimelinePostDataNotExistException.cs | 10 + BackEnd/Timeline/Services/TimelinePostService.cs | 225 ++++++++++----------- 5 files changed, 130 insertions(+), 129 deletions(-) (limited to 'BackEnd/Timeline/Services/TimelinePostService.cs') diff --git a/BackEnd/Timeline/Entities/TimelinePostDataEntity.cs b/BackEnd/Timeline/Entities/TimelinePostDataEntity.cs index 6ae8fd24..9bc5d3e8 100644 --- a/BackEnd/Timeline/Entities/TimelinePostDataEntity.cs +++ b/BackEnd/Timeline/Entities/TimelinePostDataEntity.cs @@ -10,11 +10,12 @@ namespace Timeline.Entities [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] public long Id { get; set; } + [Required] [Column("post")] public long PostId { get; set; } [ForeignKey(nameof(PostId))] - public TimelinePostEntity Timeline { get; set; } = default!; + public TimelinePostEntity Post { get; set; } = default!; [Column("index")] public long Index { get; set; } diff --git a/BackEnd/Timeline/Services/DataManager.cs b/BackEnd/Timeline/Services/DataManager.cs index f24bb59b..b697630c 100644 --- a/BackEnd/Timeline/Services/DataManager.cs +++ b/BackEnd/Timeline/Services/DataManager.cs @@ -22,20 +22,22 @@ namespace Timeline.Services /// increases its ref count and returns a tag to the entry. /// /// The data. Can't be null. + /// If true save database change. Otherwise it does not save database change. /// The tag of the created entry. /// Thrown when is null. - public Task RetainEntry(byte[] data); + public Task RetainEntry(byte[] data, bool saveDatabaseChange = true); /// /// Decrease the the ref count of the entry. /// Remove it if ref count is zero. /// /// The tag of the entry. + /// If true save database change. Otherwise it does not save database change. /// Thrown when is null. /// /// It's no-op if entry with tag does not exist. /// - public Task FreeEntry(string tag); + public Task FreeEntry(string tag, bool saveDatabaseChange = true); /// /// Retrieve the entry with given tag. If not exist, returns null. @@ -57,7 +59,7 @@ namespace Timeline.Services _eTagGenerator = eTagGenerator; } - public async Task RetainEntry(byte[] data) + public async Task RetainEntry(byte[] data, bool saveDatabaseChange = true) { if (data == null) throw new ArgumentNullException(nameof(data)); @@ -80,11 +82,14 @@ namespace Timeline.Services { entity.Ref += 1; } - await _database.SaveChangesAsync(); + + if (saveDatabaseChange) + await _database.SaveChangesAsync(); + return tag; } - public async Task FreeEntry(string tag) + public async Task FreeEntry(string tag, bool saveDatabaseChange) { if (tag == null) throw new ArgumentNullException(nameof(tag)); @@ -101,7 +106,9 @@ namespace Timeline.Services { entity.Ref -= 1; } - await _database.SaveChangesAsync(); + + if (saveDatabaseChange) + await _database.SaveChangesAsync(); } } diff --git a/BackEnd/Timeline/Services/TimelinePostCreateDataException.cs b/BackEnd/Timeline/Services/TimelinePostCreateDataException.cs index fd1e6664..10a09de7 100644 --- a/BackEnd/Timeline/Services/TimelinePostCreateDataException.cs +++ b/BackEnd/Timeline/Services/TimelinePostCreateDataException.cs @@ -6,7 +6,7 @@ namespace Timeline.Services public TimelinePostCreateDataException() { } public TimelinePostCreateDataException(string message) : base(message) { } public TimelinePostCreateDataException(string message, System.Exception inner) : base(message, inner) { } - public TimelinePostCreateDataException(long index, string? message, System.Exception? inner = null) : base(message, inner) { Index = index; } + public TimelinePostCreateDataException(long index, string? message, System.Exception? inner = null) : base($"Data at index {index} is invalid.{(message is null ? "" : " " + message)}", inner) { Index = index; } protected TimelinePostCreateDataException( System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) : base(info, context) { } diff --git a/BackEnd/Timeline/Services/TimelinePostDataNotExistException.cs b/BackEnd/Timeline/Services/TimelinePostDataNotExistException.cs index eac7a771..c70f5d9c 100644 --- a/BackEnd/Timeline/Services/TimelinePostDataNotExistException.cs +++ b/BackEnd/Timeline/Services/TimelinePostDataNotExistException.cs @@ -8,8 +8,18 @@ namespace Timeline.Services public TimelinePostDataNotExistException() : this(null, null) { } public TimelinePostDataNotExistException(string? message) : this(message, null) { } public TimelinePostDataNotExistException(string? message, Exception? inner) : base(message, inner) { } + public TimelinePostDataNotExistException(long timelineId, long postId, long dataIndex, string? message = null, Exception? inner = null) : base(message, inner) + { + TimelineId = timelineId; + PostId = postId; + DataIndex = dataIndex; + } protected TimelinePostDataNotExistException( System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + public long TimelineId { get; set; } + public long PostId { get; set; } + public long DataIndex { get; set; } } } diff --git a/BackEnd/Timeline/Services/TimelinePostService.cs b/BackEnd/Timeline/Services/TimelinePostService.cs index cea702a1..8afd0770 100644 --- a/BackEnd/Timeline/Services/TimelinePostService.cs +++ b/BackEnd/Timeline/Services/TimelinePostService.cs @@ -4,6 +4,7 @@ using SixLabors.ImageSharp; using System; using System.Collections.Generic; using System.Linq; +using System.Text; using System.Threading.Tasks; using Timeline.Entities; using Timeline.Helpers; @@ -62,11 +63,11 @@ namespace Timeline.Services /// /// The id of the timeline of the post. /// The id of the post. - /// If true, return the entity even if it is deleted. + /// If true, return the entity even if it is deleted. /// The post. /// Thrown when timeline does not exist. /// Thrown when post of does not exist or has been deleted. - Task GetPost(long timelineId, long postId, bool includeDelete = false); + Task GetPost(long timelineId, long postId, bool includeDeleted = false); /// /// Get the data digest of a post. @@ -201,7 +202,7 @@ namespace Timeline.Services if (!includeDeleted) { - query = query.Where(p => p.Content != null); + query = query.Where(p => !p.Deleted); } if (modifiedSince.HasValue) @@ -214,7 +215,7 @@ namespace Timeline.Services return await query.ToListAsync(); } - public async Task GetPost(long timelineId, long postId, bool includeDelete = false) + public async Task GetPost(long timelineId, long postId, bool includeDeleted = false) { await CheckTimelineExistence(timelineId); @@ -225,7 +226,7 @@ namespace Timeline.Services throw new TimelinePostNotExistException(timelineId, postId, false); } - if (!includeDelete && post.Content is null) + if (!includeDeleted && post.Deleted) { throw new TimelinePostNotExistException(timelineId, postId, true); } @@ -233,99 +234,46 @@ namespace Timeline.Services return post; } - public async Task GetPostDataETag(long timelineId, long postId) + public async Task GetPostDataDigest(long timelineId, long postId, long dataIndex) { await CheckTimelineExistence(timelineId); - var postEntity = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync(); + var postEntity = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).Select(p => new { p.Id, p.Deleted }).SingleOrDefaultAsync(); - if (postEntity == null) + if (postEntity is null) throw new TimelinePostNotExistException(timelineId, postId, false); - if (postEntity.Content == null) + if (postEntity.Deleted) throw new TimelinePostNotExistException(timelineId, postId, true); - if (postEntity.ContentType != TimelinePostDataKind.Image) - throw new TimelinePostNoDataException(ExceptionGetDataNonImagePost); + var dataEntity = await _database.TimelinePostData.Where(d => d.PostId == postEntity.Id && d.Index == dataIndex).SingleOrDefaultAsync(); - var tag = postEntity.Content; + if (dataEntity is null) + throw new TimelinePostDataNotExistException(timelineId, postId, dataIndex); - return tag; + return new CacheableDataDigest(dataEntity.DataTag, dataEntity.LastUpdated); } - public async Task GetPostData(long timelineId, long postId) + public async Task GetPostData(long timelineId, long postId, long dataIndex) { await CheckTimelineExistence(timelineId); - var postEntity = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync(); + var postEntity = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).Select(p => new { p.Id, p.Deleted }).SingleOrDefaultAsync(); - if (postEntity == null) + if (postEntity is null) throw new TimelinePostNotExistException(timelineId, postId, false); - if (postEntity.Content == null) + if (postEntity.Deleted) throw new TimelinePostNotExistException(timelineId, postId, true); - if (postEntity.ContentType != TimelinePostDataKind.Image) - throw new TimelinePostNoDataException(ExceptionGetDataNonImagePost); + var dataEntity = await _database.TimelinePostData.Where(d => d.PostId == postEntity.Id && d.Index == dataIndex).SingleOrDefaultAsync(); - var tag = postEntity.Content; + if (dataEntity is null) + throw new TimelinePostDataNotExistException(timelineId, postId, dataIndex); - byte[] data; + var data = await _dataManager.GetEntryAndCheck(dataEntity.DataTag, $"Timeline {timelineId}, post {postId}, data {dataIndex} requires this data."); - try - { - data = await _dataManager.GetEntry(tag); - } - catch (InvalidOperationException e) - { - throw new DatabaseCorruptedException(ExceptionGetDataDataEntryNotExist, e); - } - - if (postEntity.ExtraContent == null) - { - _logger.LogWarning(LogGetDataNoFormat); - var format = Image.DetectFormat(data); - postEntity.ExtraContent = format.DefaultMimeType; - await _database.SaveChangesAsync(); - } - - return new TimelinePostData - { - Data = data, - Type = postEntity.ExtraContent, - ETag = tag, - LastModified = postEntity.LastUpdated - }; - } - - private async Task SaveContent(TimelinePostEntity entity, TimelinePostCreateRequestData content) - { - switch (content) - { - case TimelinePostCreateRequestTextData c: - entity.ContentType = c.Kind; - entity.Content = c.Data; - break; - case TimelinePostCreateRequestImageData c: - var imageFormat = await _imageValidator.Validate(c.Data); - var imageFormatText = imageFormat.DefaultMimeType; - - var tag = await _dataManager.RetainEntry(c.Data); - - entity.ContentType = content.Kind; - entity.Content = tag; - entity.ExtraContent = imageFormatText; - break; - default: - throw new ArgumentException("Unknown content type.", nameof(content)); - }; - } - - private async Task CleanContent(TimelinePostEntity entity) - { - if (entity.Content is not null && entity.ContentType == TimelinePostDataKind.Image) - await _dataManager.FreeEntry(entity.Content); - entity.Content = null; + return new ByteData(data, dataEntity.Kind); } public async Task CreatePost(long timelineId, long authorId, TimelinePostCreateRequest request) @@ -333,15 +281,55 @@ namespace Timeline.Services if (request is null) throw new ArgumentNullException(nameof(request)); - - if (request.Content is null) - throw new ArgumentException("Content is null.", nameof(request)); - { if (!_colorValidator.Validate(request.Color, out var message)) throw new ArgumentException("Color is not valid.", nameof(request)); } + if (request.DataList is null) + throw new ArgumentException("Data list can't be null.", nameof(request)); + + if (request.DataList.Count == 0) + throw new ArgumentException("Data list can't be empty.", nameof(request)); + + if (request.DataList.Count > 100) + throw new ArgumentException("Data list count can't be bigger than 100.", nameof(request)); + + for (int index = 0; index < request.DataList.Count; index++) + { + var data = request.DataList[index]; + + switch (data.ContentType) + { + case MimeTypes.ImageGif: + case MimeTypes.ImageJpeg: + case MimeTypes.ImagePng: + case MimeTypes.ImageWebp: + try + { + await _imageValidator.Validate(data.Data, data.ContentType); + } + catch (ImageException e) + { + throw new TimelinePostCreateDataException(index, "Image validation failed.", e); + } + break; + case MimeTypes.TextPlain: + case MimeTypes.TextMarkdown: + try + { + new UTF8Encoding(false, true).GetString(data.Data); + } + catch (DecoderFallbackException e) + { + throw new TimelinePostCreateDataException(index, "Text is not a valid utf-8 sequence.", e); + } + break; + default: + throw new TimelinePostCreateDataException(index, "Unsupported content type."); + } + } + request.Time = request.Time?.MyToUtc(); await CheckTimelineExistence(timelineId); @@ -361,13 +349,29 @@ namespace Timeline.Services Color = request.Color }; - await SaveContent(postEntity, request.Content); - var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync(); timelineEntity.CurrentPostLocalId += 1; postEntity.LocalId = timelineEntity.CurrentPostLocalId; - _database.TimelinePosts.Add(postEntity); + await _database.SaveChangesAsync(); + + List dataTags = new List(); + + for (int index = 0; index < request.DataList.Count; index++) + { + var data = request.DataList[index]; + + var tag = await _dataManager.RetainEntry(data.Data, false); + + _database.TimelinePostData.Add(new TimelinePostDataEntity + { + DataTag = tag, + Kind = data.ContentType, + Index = index, + PostId = postEntity.Id, + LastUpdated = currentTime, + }); + } await _database.SaveChangesAsync(); @@ -392,12 +396,10 @@ namespace Timeline.Services var entity = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync(); - await using var transaction = await _database.Database.BeginTransactionAsync(); - if (entity is null) throw new TimelinePostNotExistException(timelineId, postId, false); - if (entity.Content is null) + if (entity.Deleted) throw new TimelinePostNotExistException(timelineId, postId, true); if (request.Time.HasValue) @@ -406,18 +408,10 @@ namespace Timeline.Services if (request.Color is not null) entity.Color = request.Color; - if (request.Content is not null) - { - await CleanContent(entity); - await SaveContent(entity, request.Content); - } - entity.LastUpdated = _clock.GetCurrentTime(); await _database.SaveChangesAsync(); - await transaction.CommitAsync(); - return entity; } @@ -430,15 +424,23 @@ namespace Timeline.Services if (entity == null) throw new TimelinePostNotExistException(timelineId, postId, false); - if (entity.Content == null) + if (entity.Deleted) throw new TimelinePostNotExistException(timelineId, postId, true); await using var transaction = await _database.Database.BeginTransactionAsync(); - await CleanContent(entity); - + entity.Deleted = true; entity.LastUpdated = _clock.GetCurrentTime(); + var dataEntities = await _database.TimelinePostData.Where(d => d.PostId == entity.Id).ToListAsync(); + + foreach (var dataEntity in dataEntities) + { + await _dataManager.FreeEntry(dataEntity.DataTag, false); + } + + _database.TimelinePostData.RemoveRange(dataEntities); + await _database.SaveChangesAsync(); await transaction.CommitAsync(); @@ -446,30 +448,11 @@ namespace Timeline.Services public async Task DeleteAllPostsOfUser(long userId) { - var posts = await _database.TimelinePosts.Where(p => p.AuthorId == userId).ToListAsync(); - - var now = _clock.GetCurrentTime(); - - var dataTags = new List(); - - foreach (var post in posts) - { - if (post.Content != null) - { - if (post.ContentType == TimelinePostDataKind.Image) - { - dataTags.Add(post.Content); - } - post.Content = null; - } - post.LastUpdated = now; - } - - await _database.SaveChangesAsync(); + var postEntities = await _database.TimelinePosts.Where(p => p.AuthorId == userId).Select(p => new { p.TimelineId, p.LocalId }).ToListAsync(); - foreach (var dataTag in dataTags) + foreach (var postEntity in postEntities) { - await _dataManager.FreeEntry(dataTag); + await this.DeletePost(postEntity.TimelineId, postEntity.LocalId); } } @@ -479,9 +462,9 @@ namespace Timeline.Services var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync(); - var postEntity = await _database.TimelinePosts.Where(p => p.Id == postId).Select(p => new { p.Content, p.AuthorId }).SingleOrDefaultAsync(); + var postEntity = await _database.TimelinePosts.Where(p => p.Id == postId).Select(p => new { p.Deleted, p.AuthorId }).SingleOrDefaultAsync(); - if (postEntity == null) + if (postEntity is null) { if (throwOnPostNotExist) throw new TimelinePostNotExistException(timelineId, postId, false); @@ -489,7 +472,7 @@ namespace Timeline.Services return true; } - if (postEntity.Content == null && throwOnPostNotExist) + if (postEntity.Deleted && throwOnPostNotExist) { throw new TimelinePostNotExistException(timelineId, postId, true); } -- cgit v1.2.3 From b5376f71157f68f06aa04bde79389f9ab291d84a Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 11 Feb 2021 22:21:12 +0800 Subject: ... --- .../IntegratedTests/TimelinePostTest.cs | 338 ++++----------------- .../IntegratedTests/UserAvatarTest.cs | 17 +- .../Timeline/Controllers/UserAvatarController.cs | 4 +- BackEnd/Timeline/Entities/TimelinePostEntity.cs | 2 + .../Timeline/Formatters/ByteDataInputFormatter.cs | 6 + BackEnd/Timeline/Models/Http/HttpTimelinePost.cs | 5 + .../Models/Http/HttpTimelinePostCreateRequest.cs | 2 + BackEnd/Timeline/Models/Mapper/TimelineMapper.cs | 2 + BackEnd/Timeline/Services/TimelinePostService.cs | 4 +- 9 files changed, 87 insertions(+), 293 deletions(-) (limited to 'BackEnd/Timeline/Services/TimelinePostService.cs') diff --git a/BackEnd/Timeline.Tests/IntegratedTests/TimelinePostTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/TimelinePostTest.cs index 4caff416..bd79ae18 100644 --- a/BackEnd/Timeline.Tests/IntegratedTests/TimelinePostTest.cs +++ b/BackEnd/Timeline.Tests/IntegratedTests/TimelinePostTest.cs @@ -1,44 +1,43 @@ using FluentAssertions; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Formats.Png; using System; using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Text; using System.Threading.Tasks; -using Timeline.Entities; using Timeline.Models; using Timeline.Models.Http; -using Timeline.Tests.Helpers; using Xunit; +using Xunit.Abstractions; namespace Timeline.Tests.IntegratedTests { - public static class TimelineHelper + public class TimelinePostTest : BaseTimelineTest { - public static HttpTimelinePostContent TextPostContent(string text) + private static HttpTimelinePostCreateRequest CreateTextPostRequest(string text, DateTime? time = null, string? color = null) { - return new HttpTimelinePostContent("text", text, null, null); - } - - public static HttpTimelinePostCreateRequest TextPostCreateRequest(string text, DateTime? time = null) - { - return new HttpTimelinePostCreateRequest + return new HttpTimelinePostCreateRequest() { - Content = new HttpTimelinePostCreateRequestContent + Time = time, + Color = color, + DataList = new List() { - Type = "text", - Text = text - }, - Time = time + new HttpTimelinePostCreateRequestData() + { + ContentType = MimeTypes.TextPlain, + Data = Convert.ToBase64String(Encoding.UTF8.GetBytes(text)) + } + } }; } - } - public class TimelinePostTest : BaseTimelineTest - { + private readonly ITestOutputHelper _outputHelper; + + public TimelinePostTest(ITestOutputHelper outputHelper) + { + _outputHelper = outputHelper; + } + [Theory] [MemberData(nameof(TimelineNameGeneratorTestData))] public async Task GetPostsAndVisibility_Should_Work(TimelineNameGenerator generator) @@ -102,15 +101,13 @@ namespace Timeline.Tests.IntegratedTests { using var client = await CreateClientAsUser(); - var postContentList = new List { "a", "b", "c", "d" }; var posts = new List(); - foreach (var content in postContentList) + for (int i = 0; i < 4; i++) { - var post = await client.TestPostAsync($"timelines/{generator(1)}/posts", - new HttpTimelinePostCreateRequest { Content = new HttpTimelinePostCreateRequestContent { Text = content, Type = TimelinePostDataKind.Text } }); + var post = await client.TestPostAsync($"timelines/{generator(1)}/posts", CreateTextPostRequest("a")); posts.Add(post); - await Task.Delay(1000); + await Task.Delay(TimeSpan.FromSeconds(1)); } await client.TestDeleteAsync($"timelines/{generator(1)}/posts/{posts[2].Id}"); @@ -118,64 +115,57 @@ namespace Timeline.Tests.IntegratedTests { var body = await client.TestGetAsync>($"timelines/{generator(1)}/posts?modifiedSince={posts[1].LastUpdated.ToString("s", CultureInfo.InvariantCulture) }"); body.Should().HaveCount(2) - .And.Subject.Select(p => p.Content!.Text).Should().Equal("b", "d"); + .And.Subject.Select(p => p.Id).Should().Equal(posts[1].Id, posts[3].Id); } } [Theory] [MemberData(nameof(TimelineNameGeneratorTestData))] - public async Task Post_ModifiedSince_And_IncludeDeleted(TimelineNameGenerator generator) + public async Task PostList_IncludeDeleted(TimelineNameGenerator generator) { using var client = await CreateClientAsUser(); - var postContentList = new List { "a", "b", "c", "d" }; var posts = new List(); - foreach (var (content, index) in postContentList.Select((v, i) => (v, i))) + for (int i = 0; i < 4; i++) { - var post = await client.TestPostAsync($"timelines/{generator(1)}/posts", - new HttpTimelinePostCreateRequest { Content = new HttpTimelinePostCreateRequestContent { Text = content, Type = TimelinePostDataKind.Text } }); - posts.Add(post); - await Task.Delay(1000); + var body = await client.TestPostAsync($"timelines/{generator(1)}/posts", CreateTextPostRequest("a")); + posts.Add(body); } - await client.TestDeleteAsync($"timelines/{generator(1)}/posts/{posts[2].Id}"); - + foreach (var id in new long[] { posts[0].Id, posts[2].Id }) { + await client.TestDeleteAsync($"timelines/{generator(1)}/posts/{id}"); + } - posts = await client.TestGetAsync>($"timelines/{generator(1)}/posts?modifiedSince={posts[1].LastUpdated.ToString("s", CultureInfo.InvariantCulture)}&includeDeleted=true"); - posts.Should().HaveCount(3); - posts.Select(p => p.Deleted).Should().Equal(false, true, false); - posts.Select(p => p.Content == null).Should().Equal(false, true, false); + { + posts = await client.TestGetAsync>($"timelines/{generator(1)}/posts?includeDeleted=true"); + posts.Should().HaveCount(4); + posts.Select(p => p.Deleted).Should().Equal(true, false, true, false); } } [Theory] [MemberData(nameof(TimelineNameGeneratorTestData))] - public async Task PostList_IncludeDeleted(TimelineNameGenerator generator) + public async Task Post_ModifiedSince_And_IncludeDeleted(TimelineNameGenerator generator) { using var client = await CreateClientAsUser(); - var postContentList = new List { "a", "b", "c", "d" }; var posts = new List(); - foreach (var content in postContentList) + for (int i = 0; i < 4; i++) { - var body = await client.TestPostAsync($"timelines/{generator(1)}/posts", - new HttpTimelinePostCreateRequest { Content = new HttpTimelinePostCreateRequestContent { Text = content, Type = TimelinePostDataKind.Text } }); - posts.Add(body); + var post = await client.TestPostAsync($"timelines/{generator(1)}/posts", CreateTextPostRequest("a")); + posts.Add(post); + await Task.Delay(TimeSpan.FromSeconds(1)); } - foreach (var id in new long[] { posts[0].Id, posts[2].Id }) - { - await client.TestDeleteAsync($"timelines/{generator(1)}/posts/{id}"); - } + await client.TestDeleteAsync($"timelines/{generator(1)}/posts/{posts[2].Id}"); { - posts = await client.TestGetAsync>($"timelines/{generator(1)}/posts?includeDeleted=true"); - posts.Should().HaveCount(4); - posts.Select(p => p.Deleted).Should().Equal(true, false, true, false); - posts.Select(p => p.Content == null).Should().Equal(true, false, true, false); + posts = await client.TestGetAsync>($"timelines/{generator(1)}/posts?modifiedSince={posts[1].LastUpdated.ToString("s", CultureInfo.InvariantCulture)}&includeDeleted=true"); + posts.Should().HaveCount(3); + posts.Select(p => p.Deleted).Should().Equal(false, true, false); } } @@ -190,25 +180,25 @@ namespace Timeline.Tests.IntegratedTests using (var client = await CreateDefaultClient()) { // no auth should get 401 - await client.TestPostAssertUnauthorizedAsync($"timelines/{generator(1)}/posts", TimelineHelper.TextPostCreateRequest("aaa")); + await client.TestPostAssertUnauthorizedAsync($"timelines/{generator(1)}/posts", CreateTextPostRequest("aaa")); } using (var client = await CreateClientAsUser()) { // post self's - await client.TestPostAsync($"timelines/{generator(1)}/posts", TimelineHelper.TextPostCreateRequest("aaa")); + await client.TestPostAsync($"timelines/{generator(1)}/posts", CreateTextPostRequest("aaa")); // post other not as a member should get 403 - await client.TestPostAssertForbiddenAsync($"timelines/{generator(0)}/posts", TimelineHelper.TextPostCreateRequest("aaa")); + await client.TestPostAssertForbiddenAsync($"timelines/{generator(0)}/posts", CreateTextPostRequest("aaa")); } using (var client = await CreateClientAsAdministrator()) { // post as admin - await client.TestPostAsync($"timelines/{generator(1)}/posts", TimelineHelper.TextPostCreateRequest("aaa")); + await client.TestPostAsync($"timelines/{generator(1)}/posts", CreateTextPostRequest("aaa")); } using (var client = await CreateClientAs(2)) { // post as member - await client.TestPostAsync($"timelines/{generator(1)}/posts", TimelineHelper.TextPostCreateRequest("aaa")); + await client.TestPostAsync($"timelines/{generator(1)}/posts", CreateTextPostRequest("aaa")); } } @@ -219,7 +209,7 @@ namespace Timeline.Tests.IntegratedTests async Task CreatePost(int userNumber) { using var client = await CreateClientAs(userNumber); - var body = await client.TestPostAsync($"timelines/{generator(1)}/posts", TimelineHelper.TextPostCreateRequest("aaa")); + var body = await client.TestPostAsync($"timelines/{generator(1)}/posts", CreateTextPostRequest("aaa")); return body.Id; } @@ -265,57 +255,6 @@ namespace Timeline.Tests.IntegratedTests } } - [Theory] - [MemberData(nameof(TimelineNameGeneratorTestData))] - public async Task TextPost_Should_Work(TimelineNameGenerator generator) - { - using var client = await CreateClientAsUser(); - - { - var body = await client.TestGetAsync>($"timelines/{generator(1)}/posts"); - body.Should().BeEmpty(); - } - - const string mockContent = "aaa"; - HttpTimelinePost createRes; - { - var body = await client.TestPostAsync($"timelines/{generator(1)}/posts", TimelineHelper.TextPostCreateRequest(mockContent)); - body.Content.Should().BeEquivalentTo(TimelineHelper.TextPostContent(mockContent)); - body.Author.Should().BeEquivalentTo(await client.GetUserAsync("user1")); - body.Deleted.Should().BeFalse(); - createRes = body; - } - { - var body = await client.TestGetAsync>($"timelines/{generator(1)}/posts"); - body.Should().BeEquivalentTo(createRes); - } - const string mockContent2 = "bbb"; - var mockTime2 = DateTime.UtcNow.AddDays(-1); - HttpTimelinePost createRes2; - { - var body = await client.TestPostAsync($"timelines/{generator(1)}/posts", TimelineHelper.TextPostCreateRequest(mockContent2, mockTime2)); - body.Should().NotBeNull(); - body.Content.Should().BeEquivalentTo(TimelineHelper.TextPostContent(mockContent2)); - body.Author.Should().BeEquivalentTo(await client.GetUserAsync("user1")); - body.Time.Should().BeCloseTo(mockTime2, 1000); - body.Deleted.Should().BeFalse(); - createRes2 = body; - } - { - var body = await client.TestGetAsync>($"timelines/{generator(1)}/posts"); - body.Should().BeEquivalentTo(createRes, createRes2); - } - { - await client.TestDeleteAsync($"timelines/{generator(1)}/posts/{createRes.Id}"); - await client.TestDeleteAssertErrorAsync($"timelines/{generator(1)}/posts/{createRes.Id}"); - await client.TestDeleteAssertErrorAsync($"timelines/{generator(1)}/posts/30000"); - } - { - var body = await client.TestGetAsync>($"timelines/{generator(1)}/posts"); - body.Should().BeEquivalentTo(createRes2); - } - } - [Theory] [MemberData(nameof(TimelineNameGeneratorTestData))] public async Task GetPost_Should_Ordered(TimelineNameGenerator generator) @@ -324,7 +263,7 @@ namespace Timeline.Tests.IntegratedTests async Task CreatePost(DateTime time) { - var body = await client.TestPostAsync($"timelines/{generator(1)}/posts", TimelineHelper.TextPostCreateRequest("aaa", time)); + var body = await client.TestPostAsync($"timelines/{generator(1)}/posts", CreateTextPostRequest("aaa", time)); return body.Id; } @@ -339,166 +278,19 @@ namespace Timeline.Tests.IntegratedTests } } - [Theory] - [MemberData(nameof(TimelineNameGeneratorTestData))] - public async Task CreatePost_InvalidModel(TimelineNameGenerator generator) - { - using var client = await CreateClientAsUser(); - var postUrl = $"timelines/{generator(1)}/posts"; - await client.TestPostAssertInvalidModelAsync(postUrl, new HttpTimelinePostCreateRequest { Content = null! }); - await client.TestPostAssertInvalidModelAsync(postUrl, new HttpTimelinePostCreateRequest { Content = new HttpTimelinePostCreateRequestContent { Type = null! } }); - await client.TestPostAssertInvalidModelAsync(postUrl, new HttpTimelinePostCreateRequest { Content = new HttpTimelinePostCreateRequestContent { Type = "hahaha" } }); - await client.TestPostAssertInvalidModelAsync(postUrl, new HttpTimelinePostCreateRequest { Content = new HttpTimelinePostCreateRequestContent { Type = "text", Text = null } }); - await client.TestPostAssertInvalidModelAsync(postUrl, new HttpTimelinePostCreateRequest { Content = new HttpTimelinePostCreateRequestContent { Type = "image", Data = null } }); - // image not base64 - await client.TestPostAssertInvalidModelAsync(postUrl, new HttpTimelinePostCreateRequest { Content = new HttpTimelinePostCreateRequestContent { Type = "image", Data = "!!!" } }); - // image base64 not image - await client.TestPostAssertInvalidModelAsync(postUrl, new HttpTimelinePostCreateRequest { Content = new HttpTimelinePostCreateRequestContent { Type = "image", Data = Convert.ToBase64String(new byte[] { 0x01, 0x02, 0x03 }) } }); - } - - [Theory] - [MemberData(nameof(TimelineNameGeneratorTestData))] - public async Task ImagePost_ShouldWork(TimelineNameGenerator generator) - { - var imageData = ImageHelper.CreatePngWithSize(100, 200); - - long postId; - string postImageUrl; - - void AssertPostContent(HttpTimelinePostContent content) - { - content.Type.Should().Be(TimelinePostDataKind.Image); - content.Url.Should().EndWith($"timelines/{generator(1)}/posts/{postId}/data"); - content.Text.Should().Be(null); - } - - using var client = await CreateClientAsUser(); - - { - var body = await client.TestPostAsync($"timelines/{generator(1)}/posts", - new HttpTimelinePostCreateRequest - { - Content = new HttpTimelinePostCreateRequestContent - { - Type = TimelinePostDataKind.Image, - Data = Convert.ToBase64String(imageData) - } - }); - postId = body.Id; - postImageUrl = body.Content!.Url!; - AssertPostContent(body.Content); - } - - { - var body = await client.TestGetAsync>($"timelines/{generator(1)}/posts"); - body.Should().HaveCount(1); - var post = body[0]; - post.Id.Should().Be(postId); - AssertPostContent(post.Content!); - } - - { - var res = await client.GetAsync($"timelines/{generator(1)}/posts/{postId}/data"); - res.Content.Headers.ContentType!.MediaType.Should().Be("image/png"); - var data = await res.Content.ReadAsByteArrayAsync(); - var image = Image.Load(data, out var format); - image.Width.Should().Be(100); - image.Height.Should().Be(200); - format.Name.Should().Be(PngFormat.Instance.Name); - } - - await CacheTestHelper.TestCache(client, $"timelines/{generator(1)}/posts/{postId}/data"); - await client.TestDeleteAsync($"timelines/{generator(1)}/posts/{postId}"); - await client.TestDeleteAssertErrorAsync($"timelines/{generator(1)}/posts/{postId}"); - - { - var body = await client.TestGetAsync>($"timelines/{generator(1)}/posts"); - body.Should().BeEmpty(); - } - - { - using var scope = TestApp.Host.Services.CreateScope(); - var database = scope.ServiceProvider.GetRequiredService(); - var count = await database.Data.CountAsync(); - count.Should().Be(0); - } - } - - [Theory] - [MemberData(nameof(TimelineNameGeneratorTestData))] - public async Task ImagePost_400(TimelineNameGenerator generator) - { - using var client = await CreateClientAsUser(); - - await client.TestGetAssertNotFoundAsync($"timelines/{generator(1)}/posts/11234/data", errorCode: ErrorCodes.TimelineController.PostNotExist); - - long postId; - { - var body = await client.TestPostAsync($"timelines/{generator(1)}/posts", TimelineHelper.TextPostCreateRequest("aaa")); - postId = body.Id; - } - - await client.TestGetAssertErrorAsync($"timelines/{generator(1)}/posts/{postId}/data", errorCode: ErrorCodes.TimelineController.PostNoData); - } - - [Theory] - [MemberData(nameof(TimelineNameGeneratorTestData))] - public async Task PostDataETag(TimelineNameGenerator generator) - { - using var client = await CreateClientAsUser(); - - long id; - string etag; - - { - var body = await client.TestPostAsync($"timelines/{generator(1)}/posts", new HttpTimelinePostCreateRequest - { - Content = new HttpTimelinePostCreateRequestContent - { - Type = TimelinePostDataKind.Image, - Data = Convert.ToBase64String(ImageHelper.CreatePngWithSize(100, 50)) - } - }); - body.Content!.ETag.Should().NotBeNullOrEmpty(); - - id = body.Id; - etag = body.Content.ETag!; - } - - { - var res = await client.GetAsync($"timelines/{generator(1)}/posts/{id}/data"); - res.StatusCode.Should().Be(200); - res.Headers.ETag.Should().NotBeNull(); - res.Headers.ETag!.ToString().Should().Be(etag); - } - } - [Theory] [MemberData(nameof(TimelineNameGeneratorTestData))] public async Task Color(TimelineNameGenerator generator) { using var client = await CreateClientAsUser(); - HttpTimelinePostCreateRequestContent CreateRequestContent() => new() - { - Type = "text", - Text = "aaa" - }; - - await client.TestPostAssertInvalidModelAsync($"timelines/{generator(1)}/posts", new HttpTimelinePostCreateRequest - { - Content = CreateRequestContent(), - Color = "#1" - }); + await client.TestPostAssertInvalidModelAsync($"timelines/{generator(1)}/posts", CreateTextPostRequest("a", color: "aa")); long id; { - var post = await client.TestPostAsync($"timelines/{generator(1)}/posts", new HttpTimelinePostCreateRequest - { - Content = CreateRequestContent(), - Color = "#aabbcc" - }); + var post = await client.TestPostAsync($"timelines/{generator(1)}/posts", + CreateTextPostRequest("a", color: "#aabbcc")); post.Color.Should().Be("#aabbcc"); id = post.Id; } @@ -515,18 +307,10 @@ namespace Timeline.Tests.IntegratedTests { using var client = await CreateClientAsUser(); - HttpTimelinePostCreateRequestContent CreateRequestContent() => new() - { - Type = "text", - Text = "aaa" - }; await client.TestGetAssertNotFoundAsync($"timelines/{generator(1)}/posts/1"); - var post = await client.TestPostAsync($"timelines/{generator(1)}/posts", new HttpTimelinePostCreateRequest - { - Content = CreateRequestContent(), - }); + var post = await client.TestPostAsync($"timelines/{generator(1)}/posts", CreateTextPostRequest("a")); var post2 = await client.TestGetAsync($"timelines/{generator(1)}/posts/{post.Id}"); post2.Should().BeEquivalentTo(post); @@ -542,14 +326,8 @@ namespace Timeline.Tests.IntegratedTests { using var client = await CreateClientAsUser(); - var post = await client.TestPostAsync($"timelines/{generator(1)}/posts", new HttpTimelinePostCreateRequest - { - Content = new() - { - Type = "text", - Text = "aaa" - } - }); + var post = await client.TestPostAsync($"timelines/{generator(1)}/posts", + CreateTextPostRequest("a")); var date = new DateTime(2000, 10, 1); diff --git a/BackEnd/Timeline.Tests/IntegratedTests/UserAvatarTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/UserAvatarTest.cs index 893a5d28..708120b1 100644 --- a/BackEnd/Timeline.Tests/IntegratedTests/UserAvatarTest.cs +++ b/BackEnd/Timeline.Tests/IntegratedTests/UserAvatarTest.cs @@ -11,8 +11,8 @@ using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; +using Timeline.Models; using Timeline.Models.Http; -using Timeline.Services; using Timeline.Tests.Helpers; using Xunit; @@ -23,11 +23,10 @@ namespace Timeline.Tests.IntegratedTests [Fact] public async Task Test() { - Avatar mockAvatar = new Avatar - { - Data = ImageHelper.CreatePngWithSize(100, 100), - Type = PngFormat.Instance.DefaultMimeType - }; + ByteData mockAvatar = new ByteData( + ImageHelper.CreatePngWithSize(100, 100), + PngFormat.Instance.DefaultMimeType + ); using (var client = await CreateClientAsUser()) { @@ -106,7 +105,7 @@ namespace Timeline.Tests.IntegratedTests } { - await client.TestPutByteArrayAsync("users/user1/avatar", mockAvatar.Data, mockAvatar.Type); + await client.TestPutByteArrayAsync("users/user1/avatar", mockAvatar.Data, mockAvatar.ContentType); await TestAvatar("user1", mockAvatar.Data); } @@ -137,7 +136,7 @@ namespace Timeline.Tests.IntegratedTests // Authorization check. using (var client = await CreateClientAsAdministrator()) { - await client.TestPutByteArrayAsync("users/user1/avatar", mockAvatar.Data, mockAvatar.Type); + await client.TestPutByteArrayAsync("users/user1/avatar", mockAvatar.Data, mockAvatar.ContentType); await client.TestDeleteAsync("users/user1/avatar"); await client.TestPutByteArrayAssertErrorAsync("users/usernotexist/avatar", new[] { (byte)0x00 }, "image/png", errorCode: ErrorCodes.UserCommon.NotExist); await client.TestDeleteAssertErrorAsync("users/usernotexist/avatar", errorCode: ErrorCodes.UserCommon.NotExist); @@ -175,4 +174,4 @@ namespace Timeline.Tests.IntegratedTests } } } -} \ No newline at end of file +} diff --git a/BackEnd/Timeline/Controllers/UserAvatarController.cs b/BackEnd/Timeline/Controllers/UserAvatarController.cs index 8ac2d21a..180d1f9b 100644 --- a/BackEnd/Timeline/Controllers/UserAvatarController.cs +++ b/BackEnd/Timeline/Controllers/UserAvatarController.cs @@ -102,12 +102,12 @@ namespace Timeline.Controllers try { - var etag = await _service.SetAvatar(id, body); + var digest = await _service.SetAvatar(id, body); _logger.LogInformation(Log.Format(LogPutSuccess, ("Username", username), ("Mime Type", Request.ContentType))); - Response.Headers.Append("ETag", new EntityTagHeaderValue($"\"{etag}\"").ToString()); + Response.Headers.Append("ETag", new EntityTagHeaderValue($"\"{digest.ETag}\"").ToString()); return Ok(); } diff --git a/BackEnd/Timeline/Entities/TimelinePostEntity.cs b/BackEnd/Timeline/Entities/TimelinePostEntity.cs index c65ef929..1f0270cb 100644 --- a/BackEnd/Timeline/Entities/TimelinePostEntity.cs +++ b/BackEnd/Timeline/Entities/TimelinePostEntity.cs @@ -37,6 +37,8 @@ namespace Timeline.Entities [Column("last_updated")] public DateTime LastUpdated { get; set; } +#pragma warning disable CA2227 public List DataList { get; set; } = default!; +#pragma warning restore CA2227 } } diff --git a/BackEnd/Timeline/Formatters/ByteDataInputFormatter.cs b/BackEnd/Timeline/Formatters/ByteDataInputFormatter.cs index 2451ead6..49f8221a 100644 --- a/BackEnd/Timeline/Formatters/ByteDataInputFormatter.cs +++ b/BackEnd/Timeline/Formatters/ByteDataInputFormatter.cs @@ -44,6 +44,12 @@ namespace Timeline.Formatters var logger = context.HttpContext.RequestServices.GetRequiredService>(); + if (request.ContentType is null) + { + logger.LogInformation("Failed to read body as bytes. Content-Type is not set."); + return await InputFormatterResult.FailureAsync(); + } + if (contentLength == null) { logger.LogInformation("Failed to read body as bytes. Content-Length is not set."); diff --git a/BackEnd/Timeline/Models/Http/HttpTimelinePost.cs b/BackEnd/Timeline/Models/Http/HttpTimelinePost.cs index 165c92da..26e1a92d 100644 --- a/BackEnd/Timeline/Models/Http/HttpTimelinePost.cs +++ b/BackEnd/Timeline/Models/Http/HttpTimelinePost.cs @@ -26,7 +26,12 @@ namespace Timeline.Models.Http /// Post id. /// public long Id { get; set; } + /// + /// The data list. + /// +#pragma warning disable CA2227 public List DataList { get; set; } = default!; +#pragma warning restore CA2227 /// /// True if post is deleted. /// diff --git a/BackEnd/Timeline/Models/Http/HttpTimelinePostCreateRequest.cs b/BackEnd/Timeline/Models/Http/HttpTimelinePostCreateRequest.cs index 07d823ad..2a973c72 100644 --- a/BackEnd/Timeline/Models/Http/HttpTimelinePostCreateRequest.cs +++ b/BackEnd/Timeline/Models/Http/HttpTimelinePostCreateRequest.cs @@ -13,7 +13,9 @@ namespace Timeline.Models.Http [Required] [MinLength(1)] [MaxLength(100)] +#pragma warning disable CA2227 public List DataList { get; set; } = default!; +#pragma warning restore CA2227 /// /// Time of the post. If not set, current time will be used. diff --git a/BackEnd/Timeline/Models/Mapper/TimelineMapper.cs b/BackEnd/Timeline/Models/Mapper/TimelineMapper.cs index 33ee9593..1f10c123 100644 --- a/BackEnd/Timeline/Models/Mapper/TimelineMapper.cs +++ b/BackEnd/Timeline/Models/Mapper/TimelineMapper.cs @@ -66,6 +66,8 @@ namespace Timeline.Models.Mapper public async Task MapToHttp(TimelinePostEntity entity, string timelineName, IUrlHelper urlHelper) { + _ = timelineName; + await _database.Entry(entity).Collection(p => p.DataList).LoadAsync(); await _database.Entry(entity).Reference(e => e.Author).LoadAsync(); diff --git a/BackEnd/Timeline/Services/TimelinePostService.cs b/BackEnd/Timeline/Services/TimelinePostService.cs index 8afd0770..62bc43cc 100644 --- a/BackEnd/Timeline/Services/TimelinePostService.cs +++ b/BackEnd/Timeline/Services/TimelinePostService.cs @@ -1,6 +1,5 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using SixLabors.ImageSharp; using System; using System.Collections.Generic; using System.Linq; @@ -12,7 +11,6 @@ using Timeline.Helpers.Cache; using Timeline.Models; using Timeline.Models.Validation; using Timeline.Services.Exceptions; -using static Timeline.Resources.Services.TimelineService; namespace Timeline.Services { @@ -37,7 +35,9 @@ namespace Timeline.Services /// If not set, current time is used. public DateTime? Time { get; set; } +#pragma warning disable CA2227 public List DataList { get; set; } = new List(); +#pragma warning restore CA2227 } public class TimelinePostPatchRequest -- cgit v1.2.3