From 4ea535d93753826ec900879560d876cec4d58c38 Mon Sep 17 00:00:00 2001 From: crupest Date: Wed, 10 Feb 2021 02:03:06 +0800 Subject: ... --- BackEnd/Timeline.ErrorCodes/ErrorCodes.cs | 1 + .../IntegratedTests/TimelinePostTest.cs | 12 +- .../Timeline/Controllers/TimelinePostController.cs | 28 ++++- .../Timeline/Controllers/UserAvatarController.cs | 15 +-- BackEnd/Timeline/Entities/DatabaseContext.cs | 1 + .../Timeline/Entities/TimelinePostDataEntity.cs | 31 +++++ BackEnd/Timeline/Entities/TimelinePostEntity.cs | 12 +- .../Timeline/Helpers/Cache/CacheableDataDigest.cs | 16 +++ BackEnd/Timeline/Helpers/Cache/DataCacheHelper.cs | 82 ++++++++++++++ .../Helpers/Cache/DelegateCacheableDataProvider.cs | 28 +++++ .../Timeline/Helpers/Cache/ICacheableDataDigest.cs | 10 ++ .../Helpers/Cache/ICacheableDataProvider.cs | 11 ++ BackEnd/Timeline/Helpers/DataCacheHelper.cs | 125 -------------------- .../Migrations/20200312112552_AddImagePost.cs | 2 +- BackEnd/Timeline/Models/Http/HttpTimelinePost.cs | 11 +- .../Models/Http/HttpTimelinePostContent.cs | 35 ------ .../Models/Http/HttpTimelinePostCreateRequest.cs | 22 +++- .../Http/HttpTimelinePostCreateRequestContent.cs | 26 ----- .../Models/Http/HttpTimelinePostDataDigest.cs | 23 ++++ BackEnd/Timeline/Models/Mapper/TimelineMapper.cs | 33 +----- .../Timeline/Models/TimelinePostContentTypes.cs | 7 +- .../Validation/TimelinePostContentTypeValidator.cs | 9 +- BackEnd/Timeline/Services/TimelinePostService.cs | 126 ++++++++------------- BackEnd/Timeline/Services/UserAvatarService.cs | 37 ++---- 24 files changed, 333 insertions(+), 370 deletions(-) create mode 100644 BackEnd/Timeline/Entities/TimelinePostDataEntity.cs create mode 100644 BackEnd/Timeline/Helpers/Cache/CacheableDataDigest.cs create mode 100644 BackEnd/Timeline/Helpers/Cache/DataCacheHelper.cs create mode 100644 BackEnd/Timeline/Helpers/Cache/DelegateCacheableDataProvider.cs create mode 100644 BackEnd/Timeline/Helpers/Cache/ICacheableDataDigest.cs create mode 100644 BackEnd/Timeline/Helpers/Cache/ICacheableDataProvider.cs delete mode 100644 BackEnd/Timeline/Helpers/DataCacheHelper.cs delete mode 100644 BackEnd/Timeline/Models/Http/HttpTimelinePostContent.cs delete mode 100644 BackEnd/Timeline/Models/Http/HttpTimelinePostCreateRequestContent.cs create mode 100644 BackEnd/Timeline/Models/Http/HttpTimelinePostDataDigest.cs (limited to 'BackEnd') diff --git a/BackEnd/Timeline.ErrorCodes/ErrorCodes.cs b/BackEnd/Timeline.ErrorCodes/ErrorCodes.cs index 53a03b69..8211a0cc 100644 --- a/BackEnd/Timeline.ErrorCodes/ErrorCodes.cs +++ b/BackEnd/Timeline.ErrorCodes/ErrorCodes.cs @@ -17,6 +17,7 @@ public static class Header { public const int IfNonMatch_BadFormat = 1_000_01_01; + public const int IfModifiedSince_BadFormat = 1_000_01_02; } public static class Content diff --git a/BackEnd/Timeline.Tests/IntegratedTests/TimelinePostTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/TimelinePostTest.cs index 17c85f22..4caff416 100644 --- a/BackEnd/Timeline.Tests/IntegratedTests/TimelinePostTest.cs +++ b/BackEnd/Timeline.Tests/IntegratedTests/TimelinePostTest.cs @@ -108,7 +108,7 @@ namespace Timeline.Tests.IntegratedTests foreach (var content in postContentList) { var post = await client.TestPostAsync($"timelines/{generator(1)}/posts", - new HttpTimelinePostCreateRequest { Content = new HttpTimelinePostCreateRequestContent { Text = content, Type = TimelinePostContentTypes.Text } }); + new HttpTimelinePostCreateRequest { Content = new HttpTimelinePostCreateRequestContent { Text = content, Type = TimelinePostDataKind.Text } }); posts.Add(post); await Task.Delay(1000); } @@ -134,7 +134,7 @@ namespace Timeline.Tests.IntegratedTests foreach (var (content, index) in postContentList.Select((v, i) => (v, i))) { var post = await client.TestPostAsync($"timelines/{generator(1)}/posts", - new HttpTimelinePostCreateRequest { Content = new HttpTimelinePostCreateRequestContent { Text = content, Type = TimelinePostContentTypes.Text } }); + new HttpTimelinePostCreateRequest { Content = new HttpTimelinePostCreateRequestContent { Text = content, Type = TimelinePostDataKind.Text } }); posts.Add(post); await Task.Delay(1000); } @@ -162,7 +162,7 @@ namespace Timeline.Tests.IntegratedTests foreach (var content in postContentList) { var body = await client.TestPostAsync($"timelines/{generator(1)}/posts", - new HttpTimelinePostCreateRequest { Content = new HttpTimelinePostCreateRequestContent { Text = content, Type = TimelinePostContentTypes.Text } }); + new HttpTimelinePostCreateRequest { Content = new HttpTimelinePostCreateRequestContent { Text = content, Type = TimelinePostDataKind.Text } }); posts.Add(body); } @@ -367,7 +367,7 @@ namespace Timeline.Tests.IntegratedTests void AssertPostContent(HttpTimelinePostContent content) { - content.Type.Should().Be(TimelinePostContentTypes.Image); + content.Type.Should().Be(TimelinePostDataKind.Image); content.Url.Should().EndWith($"timelines/{generator(1)}/posts/{postId}/data"); content.Text.Should().Be(null); } @@ -380,7 +380,7 @@ namespace Timeline.Tests.IntegratedTests { Content = new HttpTimelinePostCreateRequestContent { - Type = TimelinePostContentTypes.Image, + Type = TimelinePostDataKind.Image, Data = Convert.ToBase64String(imageData) } }); @@ -455,7 +455,7 @@ namespace Timeline.Tests.IntegratedTests { Content = new HttpTimelinePostCreateRequestContent { - Type = TimelinePostContentTypes.Image, + Type = TimelinePostDataKind.Image, Data = Convert.ToBase64String(ImageHelper.CreatePngWithSize(100, 50)) } }); diff --git a/BackEnd/Timeline/Controllers/TimelinePostController.cs b/BackEnd/Timeline/Controllers/TimelinePostController.cs index 44498c58..a0fd1687 100644 --- a/BackEnd/Timeline/Controllers/TimelinePostController.cs +++ b/BackEnd/Timeline/Controllers/TimelinePostController.cs @@ -112,7 +112,7 @@ namespace Timeline.Controllers [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 DataIndexGet([FromRoute][GeneralTimelineName] string timeline, [FromRoute] long post, [FromHeader(Name = "If-None-Match")] string? ifNoneMatch) { _ = ifNoneMatch; @@ -139,6 +139,24 @@ namespace Timeline.Controllers } } + /// + /// Get the data of a post. Usually a image post. + /// + /// Timeline name. + /// The id of the post. + /// If-None-Match header. + /// The data. + [HttpGet("{post}/data/{data_index}")] + [Produces("image/png", "image/jpeg", "image/gif", "image/webp", "application/json", "text/json")] + [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) + { + } + /// /// Create a new post. /// @@ -163,18 +181,18 @@ namespace Timeline.Controllers var requestContent = body.Content; - TimelinePostCreateRequestContent createContent; + TimelinePostCreateRequestData createContent; switch (requestContent.Type) { - case TimelinePostContentTypes.Text: + 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 TimelinePostContentTypes.Image: + case TimelinePostDataKind.Image: if (requestContent.Data is null) return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ImageContentDataRequired)); @@ -189,7 +207,7 @@ namespace Timeline.Controllers return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ImageContentDataNotBase64)); } - createContent = new TimelinePostCreateRequestImageContent(data); + createContent = new TimelinePostCreateRequestImageData(data); break; default: return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ContentUnknownType)); diff --git a/BackEnd/Timeline/Controllers/UserAvatarController.cs b/BackEnd/Timeline/Controllers/UserAvatarController.cs index f3b7fff8..8ac2d21a 100644 --- a/BackEnd/Timeline/Controllers/UserAvatarController.cs +++ b/BackEnd/Timeline/Controllers/UserAvatarController.cs @@ -7,6 +7,7 @@ using System; using System.Threading.Tasks; using Timeline.Filters; using Timeline.Helpers; +using Timeline.Helpers.Cache; using Timeline.Models; using Timeline.Models.Http; using Timeline.Models.Validation; @@ -63,11 +64,7 @@ namespace Timeline.Controllers return NotFound(ErrorResponse.UserCommon.NotExist()); } - return await DataCacheHelper.GenerateActionResult(this, () => _service.GetAvatarETag(id), async () => - { - var avatar = await _service.GetAvatar(id); - return avatar.ToCacheableData(); - }); + return await DataCacheHelper.GenerateActionResult(this, () => _service.GetAvatarDigest(id), () => _service.GetAvatar(id)); } /// @@ -105,11 +102,7 @@ namespace Timeline.Controllers try { - var etag = await _service.SetAvatar(id, new Avatar - { - Data = body.Data, - Type = body.ContentType - }); + var etag = await _service.SetAvatar(id, body); _logger.LogInformation(Log.Format(LogPutSuccess, ("Username", username), ("Mime Type", Request.ContentType))); @@ -166,7 +159,7 @@ namespace Timeline.Controllers return BadRequest(ErrorResponse.UserCommon.NotExist()); } - await _service.SetAvatar(id, null); + await _service.DeleteAvatar(id); return Ok(); } } diff --git a/BackEnd/Timeline/Entities/DatabaseContext.cs b/BackEnd/Timeline/Entities/DatabaseContext.cs index 513cdc95..a0b59d1f 100644 --- a/BackEnd/Timeline/Entities/DatabaseContext.cs +++ b/BackEnd/Timeline/Entities/DatabaseContext.cs @@ -28,6 +28,7 @@ namespace Timeline.Entities public DbSet UserPermission { get; set; } = default!; public DbSet Timelines { get; set; } = default!; public DbSet TimelinePosts { get; set; } = default!; + public DbSet TimelinePostData { get; set; } = default!; public DbSet TimelineMembers { get; set; } = default!; public DbSet HighlightTimelines { get; set; } = default!; public DbSet BookmarkTimelines { get; set; } = default!; diff --git a/BackEnd/Timeline/Entities/TimelinePostDataEntity.cs b/BackEnd/Timeline/Entities/TimelinePostDataEntity.cs new file mode 100644 index 00000000..6ae8fd24 --- /dev/null +++ b/BackEnd/Timeline/Entities/TimelinePostDataEntity.cs @@ -0,0 +1,31 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Timeline.Entities +{ + [Table("timeline_post_data")] + public class TimelinePostDataEntity + { + [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + + [Column("post")] + public long PostId { get; set; } + + [ForeignKey(nameof(PostId))] + public TimelinePostEntity Timeline { get; set; } = default!; + + [Column("index")] + public long Index { get; set; } + + [Column("kind")] + public string Kind { get; set; } = default!; + + [Column("data_tag")] + public string DataTag { get; set; } = default!; + + [Column("last_updated")] + public DateTime LastUpdated { get; set; } + } +} diff --git a/BackEnd/Timeline/Entities/TimelinePostEntity.cs b/BackEnd/Timeline/Entities/TimelinePostEntity.cs index 39b11a5b..c65ef929 100644 --- a/BackEnd/Timeline/Entities/TimelinePostEntity.cs +++ b/BackEnd/Timeline/Entities/TimelinePostEntity.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; @@ -25,14 +26,7 @@ namespace Timeline.Entities [ForeignKey(nameof(AuthorId))] public UserEntity? Author { get; set; } = default!; - [Column("content_type"), Required] - public string ContentType { get; set; } = default!; - - [Column("content")] - public string? Content { get; set; } - - [Column("extra_content")] - public string? ExtraContent { get; set; } + public bool Deleted { get; set; } [Column("color")] public string? Color { get; set; } @@ -42,5 +36,7 @@ namespace Timeline.Entities [Column("last_updated")] public DateTime LastUpdated { get; set; } + + public List DataList { get; set; } = default!; } } diff --git a/BackEnd/Timeline/Helpers/Cache/CacheableDataDigest.cs b/BackEnd/Timeline/Helpers/Cache/CacheableDataDigest.cs new file mode 100644 index 00000000..3b5bcf52 --- /dev/null +++ b/BackEnd/Timeline/Helpers/Cache/CacheableDataDigest.cs @@ -0,0 +1,16 @@ +using System; + +namespace Timeline.Helpers.Cache +{ + public class CacheableDataDigest + { + public CacheableDataDigest(string eTag, DateTime lastModified) + { + ETag = eTag; + LastModified = lastModified; + } + + public string ETag { get; set; } + public DateTime LastModified { get; set; } + } +} diff --git a/BackEnd/Timeline/Helpers/Cache/DataCacheHelper.cs b/BackEnd/Timeline/Helpers/Cache/DataCacheHelper.cs new file mode 100644 index 00000000..c26bdddc --- /dev/null +++ b/BackEnd/Timeline/Helpers/Cache/DataCacheHelper.cs @@ -0,0 +1,82 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Net.Http.Headers; +using System; +using System.Linq; +using System.Threading.Tasks; +using Timeline.Models; +using Timeline.Models.Http; + +namespace Timeline.Helpers.Cache +{ + public static class DataCacheHelper + { + public static async Task GenerateActionResult(Controller controller, ICacheableDataProvider provider, TimeSpan? maxAge = null) + { + const string CacheControlHeaderKey = "Cache-Control"; + const string IfNonMatchHeaderKey = "If-None-Match"; + const string IfModifiedSinceHeaderKey = "If-Modified-Since"; + const string ETagHeaderKey = "ETag"; + const string LastModifiedHeaderKey = "Last-Modified"; + + string GenerateCacheControlHeaderValue() + { + var cacheControlHeader = new CacheControlHeaderValue() + { + NoCache = true, + NoStore = false, + MaxAge = maxAge ?? TimeSpan.FromDays(14), + Private = true, + MustRevalidate = true + }; + return cacheControlHeader.ToString(); + } + + var digest = await provider.GetDigest(); + var eTagValue = '"' + digest.ETag + '"'; + var eTag = new EntityTagHeaderValue(eTagValue); + + ActionResult Generate304Result() + { + controller.Response.Headers.Add(ETagHeaderKey, eTagValue); + controller.Response.Headers.Add(LastModifiedHeaderKey, digest.LastModified.ToString("R")); + controller.Response.Headers.Add(CacheControlHeaderKey, GenerateCacheControlHeaderValue()); + return controller.StatusCode(StatusCodes.Status304NotModified, null); + } + + if (controller.Request.Headers.TryGetValue(IfNonMatchHeaderKey, out var ifNonMatchHeaderValue)) + { + if (!EntityTagHeaderValue.TryParseList(ifNonMatchHeaderValue, out var eTagList)) + { + return controller.BadRequest(ErrorResponse.Common.Header.IfNonMatch_BadFormat()); + } + + if (eTagList.FirstOrDefault(e => e.Equals(eTag)) != null) + { + return Generate304Result(); + } + } + else if (controller.Request.Headers.TryGetValue(IfModifiedSinceHeaderKey, out var ifModifiedSinceHeaderValue)) + { + if (!DateTime.TryParse(ifModifiedSinceHeaderValue, out var headerValue)) + { + return controller.BadRequest(new CommonResponse(ErrorCodes.Common.Header.IfModifiedSince_BadFormat, "Header If-Modified-Since is of bad format.")); + } + + if (headerValue > digest.LastModified) + { + return Generate304Result(); + } + } + + var data = await provider.GetData(); + controller.Response.Headers.Add(CacheControlHeaderKey, GenerateCacheControlHeaderValue()); + return controller.File(data.Data, data.ContentType, digest.LastModified, eTag); + } + + public static Task GenerateActionResult(Controller controller, Func> getDigestDelegate, Func> getDataDelegate, TimeSpan? maxAge = null) + { + return GenerateActionResult(controller, new DelegateCacheableDataProvider(getDigestDelegate, getDataDelegate), maxAge); + } + } +} diff --git a/BackEnd/Timeline/Helpers/Cache/DelegateCacheableDataProvider.cs b/BackEnd/Timeline/Helpers/Cache/DelegateCacheableDataProvider.cs new file mode 100644 index 00000000..80cb66c7 --- /dev/null +++ b/BackEnd/Timeline/Helpers/Cache/DelegateCacheableDataProvider.cs @@ -0,0 +1,28 @@ +using System; +using System.Threading.Tasks; +using Timeline.Models; + +namespace Timeline.Helpers.Cache +{ + public class DelegateCacheableDataProvider : ICacheableDataProvider + { + private readonly Func> _getDigestDelegate; + private readonly Func> _getDataDelegate; + + public DelegateCacheableDataProvider(Func> getDigestDelegate, Func> getDataDelegate) + { + _getDigestDelegate = getDigestDelegate; + _getDataDelegate = getDataDelegate; + } + + public Task GetDigest() + { + return _getDigestDelegate(); + } + + public Task GetData() + { + return _getDataDelegate(); + } + } +} diff --git a/BackEnd/Timeline/Helpers/Cache/ICacheableDataDigest.cs b/BackEnd/Timeline/Helpers/Cache/ICacheableDataDigest.cs new file mode 100644 index 00000000..32519d7e --- /dev/null +++ b/BackEnd/Timeline/Helpers/Cache/ICacheableDataDigest.cs @@ -0,0 +1,10 @@ +using System; + +namespace Timeline.Helpers.Cache +{ + public interface ICacheableDataDigest + { + string ETag { get; } + DateTime LastModified { get; } + } +} diff --git a/BackEnd/Timeline/Helpers/Cache/ICacheableDataProvider.cs b/BackEnd/Timeline/Helpers/Cache/ICacheableDataProvider.cs new file mode 100644 index 00000000..b270fb1d --- /dev/null +++ b/BackEnd/Timeline/Helpers/Cache/ICacheableDataProvider.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using Timeline.Models; + +namespace Timeline.Helpers.Cache +{ + public interface ICacheableDataProvider + { + Task GetDigest(); + Task GetData(); + } +} diff --git a/BackEnd/Timeline/Helpers/DataCacheHelper.cs b/BackEnd/Timeline/Helpers/DataCacheHelper.cs deleted file mode 100644 index 1ad69708..00000000 --- a/BackEnd/Timeline/Helpers/DataCacheHelper.cs +++ /dev/null @@ -1,125 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Net.Http.Headers; -using System; -using System.Linq; -using System.Threading.Tasks; -using Timeline.Models.Http; -using static Timeline.Resources.Helper.DataCacheHelper; - -namespace Timeline.Helpers -{ - public interface ICacheableData - { - string Type { get; } -#pragma warning disable CA1819 // Properties should not return arrays - byte[] Data { get; } -#pragma warning restore CA1819 // Properties should not return arrays - DateTime? LastModified { get; } - } - - public class CacheableData : ICacheableData - { - public CacheableData(string type, byte[] data, DateTime? lastModified) - { - Type = type; - Data = data; - LastModified = lastModified; - } - - public string Type { 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 DateTime? LastModified { get; set; } - } - - public interface ICacheableDataProvider - { - Task GetDataETag(); - Task GetData(); - } - - public class DelegateCacheableDataProvider : ICacheableDataProvider - { - private readonly Func> _getDataETagDelegate; - private readonly Func> _getDataDelegate; - - public DelegateCacheableDataProvider(Func> getDataETagDelegate, Func> getDataDelegate) - { - _getDataETagDelegate = getDataETagDelegate; - _getDataDelegate = getDataDelegate; - } - - public Task GetData() - { - return _getDataDelegate(); - } - - public Task GetDataETag() - { - return _getDataETagDelegate(); - } - } - - public static class DataCacheHelper - { - public static async Task GenerateActionResult(Controller controller, ICacheableDataProvider provider, TimeSpan? maxAge = null) - { - const string CacheControlHeaderKey = "Cache-Control"; - const string IfNonMatchHeaderKey = "If-None-Match"; - const string ETagHeaderKey = "ETag"; - - string GenerateCacheControlHeaderValue() - { - var cacheControlHeader = new CacheControlHeaderValue() - { - NoCache = true, - NoStore = false, - MaxAge = maxAge ?? TimeSpan.FromDays(14), - Private = true, - MustRevalidate = true - }; - return cacheControlHeader.ToString(); - } - - var loggerFactory = controller.HttpContext.RequestServices.GetRequiredService(); - var logger = loggerFactory.CreateLogger(typeof(DataCacheHelper)); - - var eTagValue = await provider.GetDataETag(); - eTagValue = '"' + eTagValue + '"'; - var eTag = new EntityTagHeaderValue(eTagValue); - - - if (controller.Request.Headers.TryGetValue(IfNonMatchHeaderKey, out var value)) - { - if (!EntityTagHeaderValue.TryParseStrictList(value, out var eTagList)) - { - logger.LogInformation(Log.Format(LogBadIfNoneMatch, ("Header Value", value))); - return controller.BadRequest(ErrorResponse.Common.Header.IfNonMatch_BadFormat()); - } - - if (eTagList.FirstOrDefault(e => e.Equals(eTag)) != null) - { - logger.LogInformation(LogResultNotModified); - controller.Response.Headers.Add(ETagHeaderKey, eTagValue); - controller.Response.Headers.Add(CacheControlHeaderKey, GenerateCacheControlHeaderValue()); - - return controller.StatusCode(StatusCodes.Status304NotModified, null); - } - } - - var data = await provider.GetData(); - logger.LogInformation(LogResultData); - controller.Response.Headers.Add(CacheControlHeaderKey, GenerateCacheControlHeaderValue()); - return controller.File(data.Data, data.Type, data.LastModified, eTag); - } - - public static Task GenerateActionResult(Controller controller, Func> getDataETagDelegate, Func> getDataDelegate, TimeSpan? maxAge = null) - { - return GenerateActionResult(controller, new DelegateCacheableDataProvider(getDataETagDelegate, getDataDelegate), maxAge); - } - } -} diff --git a/BackEnd/Timeline/Migrations/20200312112552_AddImagePost.cs b/BackEnd/Timeline/Migrations/20200312112552_AddImagePost.cs index d5098ce0..b6cc29a3 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 = '{TimelinePostContentTypes.Text}'; +SET content_type = '{TimelinePostDataKind.Text}'; "); } diff --git a/BackEnd/Timeline/Models/Http/HttpTimelinePost.cs b/BackEnd/Timeline/Models/Http/HttpTimelinePost.cs index 5981d7a4..165c92da 100644 --- a/BackEnd/Timeline/Models/Http/HttpTimelinePost.cs +++ b/BackEnd/Timeline/Models/Http/HttpTimelinePost.cs @@ -1,7 +1,9 @@ using System; +using System.Collections.Generic; namespace Timeline.Models.Http { + /// /// Info of a post. /// @@ -9,10 +11,10 @@ namespace Timeline.Models.Http { public HttpTimelinePost() { } - public HttpTimelinePost(long id, HttpTimelinePostContent? content, bool deleted, DateTime time, HttpUser? author, string? color, DateTime lastUpdated) + public HttpTimelinePost(long id, List dataList, bool deleted, DateTime time, HttpUser? author, string? color, DateTime lastUpdated) { Id = id; - Content = content; + DataList = dataList; Deleted = deleted; Time = time; Author = author; @@ -24,10 +26,7 @@ namespace Timeline.Models.Http /// Post id. /// public long Id { get; set; } - /// - /// Content of the post. May be null if post is deleted. - /// - public HttpTimelinePostContent? Content { get; set; } + public List DataList { get; set; } = default!; /// /// True if post is deleted. /// diff --git a/BackEnd/Timeline/Models/Http/HttpTimelinePostContent.cs b/BackEnd/Timeline/Models/Http/HttpTimelinePostContent.cs deleted file mode 100644 index 55ff1ac2..00000000 --- a/BackEnd/Timeline/Models/Http/HttpTimelinePostContent.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace Timeline.Models.Http -{ - /// - /// Info of post content. - /// - public class HttpTimelinePostContent - { - public HttpTimelinePostContent() { } - - public HttpTimelinePostContent(string type, string? text, string? url, string? eTag) - { - Type = type; - Text = text; - Url = url; - ETag = eTag; - } - - /// - /// Type of the post content. - /// - public string Type { get; set; } = default!; - /// - /// If post is of text type. This is the text. - /// - public string? Text { get; set; } - /// - /// If post is of image type. This is the image url. - /// - public string? Url { get; set; } - /// - /// If post has data (currently it means it's a image post), this is the data etag. - /// - public string? ETag { get; set; } - } -} diff --git a/BackEnd/Timeline/Models/Http/HttpTimelinePostCreateRequest.cs b/BackEnd/Timeline/Models/Http/HttpTimelinePostCreateRequest.cs index b25adf36..20d1a25b 100644 --- a/BackEnd/Timeline/Models/Http/HttpTimelinePostCreateRequest.cs +++ b/BackEnd/Timeline/Models/Http/HttpTimelinePostCreateRequest.cs @@ -1,16 +1,34 @@ using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; 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 { /// - /// Content of the new post. + /// Data list of the new content. /// [Required] - public HttpTimelinePostCreateRequestContent Content { get; set; } = default!; + [MinLength(1)] + public List DataList { get; set; } = default!; /// /// Time of the post. If not set, current time will be used. diff --git a/BackEnd/Timeline/Models/Http/HttpTimelinePostCreateRequestContent.cs b/BackEnd/Timeline/Models/Http/HttpTimelinePostCreateRequestContent.cs deleted file mode 100644 index 12ab407f..00000000 --- a/BackEnd/Timeline/Models/Http/HttpTimelinePostCreateRequestContent.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using Timeline.Models.Validation; - -namespace Timeline.Models.Http -{ - /// - /// Content of post create request. - /// - public class HttpTimelinePostCreateRequestContent - { - /// - /// Type of post content. - /// - [Required] - [TimelinePostContentType] - public string Type { get; set; } = default!; - /// - /// If post is of text type, this is the text. - /// - public string? Text { get; set; } - /// - /// If post is of image type, this is base64 of image data. - /// - public string? Data { get; set; } - } -} diff --git a/BackEnd/Timeline/Models/Http/HttpTimelinePostDataDigest.cs b/BackEnd/Timeline/Models/Http/HttpTimelinePostDataDigest.cs new file mode 100644 index 00000000..61d35e15 --- /dev/null +++ b/BackEnd/Timeline/Models/Http/HttpTimelinePostDataDigest.cs @@ -0,0 +1,23 @@ +using System; + +namespace Timeline.Models.Http +{ + public class HttpTimelinePostDataDigest + { + public HttpTimelinePostDataDigest() + { + + } + + public HttpTimelinePostDataDigest(string kind, string eTag, DateTime lastUpdated) + { + Kind = kind; + ETag = eTag; + LastUpdated = lastUpdated; + } + + public string Kind { get; set; } = default!; + public string ETag { get; set; } = default!; + public DateTime LastUpdated { get; set; } + } +} diff --git a/BackEnd/Timeline/Models/Mapper/TimelineMapper.cs b/BackEnd/Timeline/Models/Mapper/TimelineMapper.cs index 88c96d8a..33ee9593 100644 --- a/BackEnd/Timeline/Models/Mapper/TimelineMapper.cs +++ b/BackEnd/Timeline/Models/Mapper/TimelineMapper.cs @@ -1,7 +1,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Threading.Tasks; using Timeline.Controllers; @@ -67,34 +66,12 @@ namespace Timeline.Models.Mapper public async Task MapToHttp(TimelinePostEntity entity, string timelineName, IUrlHelper urlHelper) { - HttpTimelinePostContent? content = null; - - if (entity.Content != null) - { - content = entity.ContentType switch - { - TimelinePostContentTypes.Text => new HttpTimelinePostContent - ( - type: TimelinePostContentTypes.Text, - text: entity.Content, - url: null, - eTag: null - ), - TimelinePostContentTypes.Image => new HttpTimelinePostContent - ( - type: TimelinePostContentTypes.Image, - text: null, - url: urlHelper.ActionLink(nameof(TimelinePostController.DataGet), nameof(TimelinePostController)[0..^nameof(Controller).Length], new { timeline = timelineName, post = entity.LocalId }), - eTag: $"\"{entity.Content}\"" - ), - _ => throw new DatabaseCorruptedException(string.Format(CultureInfo.InvariantCulture, "Unknown timeline post type {0}.", entity.ContentType)) - }; - } - + await _database.Entry(entity).Collection(p => p.DataList).LoadAsync(); await _database.Entry(entity).Reference(e => e.Author).LoadAsync(); - HttpUser? author = null; + List dataDigestList = entity.DataList.OrderBy(d => d.Index).Select(d => new HttpTimelinePostDataDigest(d.Kind, d.DataTag, d.LastUpdated)).ToList(); + HttpUser? author = null; if (entity.Author is not null) { author = await _userMapper.MapToHttp(entity.Author, urlHelper); @@ -102,11 +79,11 @@ namespace Timeline.Models.Mapper return new HttpTimelinePost( id: entity.LocalId, - content: content, - deleted: content is null, + dataList: dataDigestList, time: entity.Time, author: author, color: entity.Color, + deleted: entity.Deleted, lastUpdated: entity.LastUpdated ); } diff --git a/BackEnd/Timeline/Models/TimelinePostContentTypes.cs b/BackEnd/Timeline/Models/TimelinePostContentTypes.cs index ca5e79e1..d432e03c 100644 --- a/BackEnd/Timeline/Models/TimelinePostContentTypes.cs +++ b/BackEnd/Timeline/Models/TimelinePostContentTypes.cs @@ -2,13 +2,12 @@ namespace Timeline.Models { - public static class TimelinePostContentTypes + public static class TimelinePostDataKind { -#pragma warning disable CA1819 // Properties should not return arrays - public static string[] AllTypes { get; } = new string[] { Text, Image }; -#pragma warning restore CA1819 // Properties should not return arrays + 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 index 483cce06..b65c846c 100644 --- a/BackEnd/Timeline/Models/Validation/TimelinePostContentTypeValidator.cs +++ b/BackEnd/Timeline/Models/Validation/TimelinePostContentTypeValidator.cs @@ -1,16 +1,17 @@ using System; +using System.Linq; namespace Timeline.Models.Validation { - public class TimelinePostContentTypeValidator : StringSetValidator + public class TimelinePostDataKindValidator : StringSetValidator { - public TimelinePostContentTypeValidator() : base(TimelinePostContentTypes.AllTypes) { } + public TimelinePostDataKindValidator() : base(TimelinePostDataKind.AllTypes.ToArray()) { } } [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)] - public class TimelinePostContentTypeAttribute : ValidateWithAttribute + public class TimelinePostDataKindAttribute : ValidateWithAttribute { - public TimelinePostContentTypeAttribute() : base(typeof(TimelinePostContentTypeValidator)) + public TimelinePostDataKindAttribute() : base(typeof(TimelinePostDataKindValidator)) { } 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); } diff --git a/BackEnd/Timeline/Services/UserAvatarService.cs b/BackEnd/Timeline/Services/UserAvatarService.cs index b41c45fd..afd6cf0a 100644 --- a/BackEnd/Timeline/Services/UserAvatarService.cs +++ b/BackEnd/Timeline/Services/UserAvatarService.cs @@ -8,28 +8,12 @@ using System.Linq; using System.Threading.Tasks; using Timeline.Entities; using Timeline.Helpers; +using Timeline.Helpers.Cache; +using Timeline.Models; using Timeline.Services.Exceptions; namespace Timeline.Services { - public class Avatar - { - public string Type { get; set; } = default!; - [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1819:Properties should not return arrays", Justification = "DTO Object")] - public byte[] Data { get; set; } = default!; - } - - public class AvatarInfo - { - public Avatar Avatar { get; set; } = default!; - public DateTime LastModified { get; set; } - - public CacheableData ToCacheableData() - { - return new CacheableData(Avatar.Type, Avatar.Data, LastModified); - } - } - /// /// Provider for default user avatar. /// @@ -42,29 +26,24 @@ namespace Timeline.Services /// Get the etag of default avatar. /// /// - Task GetDefaultAvatarETag(); + Task GetDefaultAvatarETag(); /// /// Get the default avatar. /// - Task GetDefaultAvatar(); + Task GetDefaultAvatar(); } public interface IUserAvatarService { - /// - /// Get the etag of a user's avatar. Warning: This method does not check the user existence. - /// - /// The id of the user to get avatar etag of. - /// The etag. - Task GetAvatarETag(long id); + Task GetAvatarDigest(long id); /// /// Get avatar of a user. If the user has no avatar set, a default one is returned. Warning: This method does not check the user existence. /// /// The id of the user to get avatar of. /// The avatar info. - Task GetAvatar(long id); + Task GetAvatar(long id); /// /// Set avatar for a user. Warning: This method does not check the user existence. @@ -74,7 +53,9 @@ namespace Timeline.Services /// The etag of the avatar. /// Thrown if any field in is null when is not null. /// Thrown if avatar is of bad format. - Task SetAvatar(long id, Avatar? avatar); + Task SetAvatar(long id, ByteData avatar); + + Task DeleteAvatar(long id); } // TODO! : Make this configurable. -- cgit v1.2.3