diff options
author | crupest <crupest@outlook.com> | 2021-02-10 02:03:06 +0800 |
---|---|---|
committer | crupest <crupest@outlook.com> | 2021-02-10 02:03:06 +0800 |
commit | 4ea535d93753826ec900879560d876cec4d58c38 (patch) | |
tree | b2d94c4347fddf8641fc41caa86826374c9cc241 | |
parent | bf8f849c36bfc42cfd0ef1a66f2adf01dc8e1be7 (diff) | |
download | timeline-4ea535d93753826ec900879560d876cec4d58c38.tar.gz timeline-4ea535d93753826ec900879560d876cec4d58c38.tar.bz2 timeline-4ea535d93753826ec900879560d876cec4d58c38.zip |
...
24 files changed, 333 insertions, 370 deletions
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<HttpTimelinePost>($"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<HttpTimelinePost>($"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<HttpTimelinePost>($"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<IActionResult> DataGet([FromRoute][GeneralTimelineName] string timeline, [FromRoute] long post, [FromHeader(Name = "If-None-Match")] string? ifNoneMatch)
+ public async Task<IActionResult> DataIndexGet([FromRoute][GeneralTimelineName] string timeline, [FromRoute] long post, [FromHeader(Name = "If-None-Match")] string? ifNoneMatch)
{
_ = ifNoneMatch;
@@ -140,6 +140,24 @@ namespace Timeline.Controllers }
/// <summary>
+ /// Get the data of a post. Usually a image post.
+ /// </summary>
+ /// <param name="timeline">Timeline name.</param>
+ /// <param name="post">The id of the post.</param>
+ /// <param name="ifNoneMatch">If-None-Match header.</param>
+ /// <returns>The data.</returns>
+ [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<IActionResult> DataGet([FromRoute][GeneralTimelineName] string timeline, [FromRoute] long post, [FromHeader(Name = "If-None-Match")] string? ifNoneMatch)
+ {
+ }
+
+ /// <summary>
/// Create a new post.
/// </summary>
/// <param name="timeline">Timeline name.</param>
@@ -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));
}
/// <summary>
@@ -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<UserPermissionEntity> UserPermission { get; set; } = default!;
public DbSet<TimelineEntity> Timelines { get; set; } = default!;
public DbSet<TimelinePostEntity> TimelinePosts { get; set; } = default!;
+ public DbSet<TimelinePostDataEntity> TimelinePostData { get; set; } = default!;
public DbSet<TimelineMemberEntity> TimelineMembers { get; set; } = default!;
public DbSet<HighlightTimelineEntity> HighlightTimelines { get; set; } = default!;
public DbSet<BookmarkTimelineEntity> 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<TimelinePostDataEntity> 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<ActionResult> 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<ActionResult> GenerateActionResult(Controller controller, Func<Task<ICacheableDataDigest>> getDigestDelegate, Func<Task<ByteData>> 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<Task<ICacheableDataDigest>> _getDigestDelegate;
+ private readonly Func<Task<ByteData>> _getDataDelegate;
+
+ public DelegateCacheableDataProvider(Func<Task<ICacheableDataDigest>> getDigestDelegate, Func<Task<ByteData>> getDataDelegate)
+ {
+ _getDigestDelegate = getDigestDelegate;
+ _getDataDelegate = getDataDelegate;
+ }
+
+ public Task<ICacheableDataDigest> GetDigest()
+ {
+ return _getDigestDelegate();
+ }
+
+ public Task<ByteData> 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<ICacheableDataDigest> GetDigest();
+ Task<ByteData> 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<string> GetDataETag();
- Task<ICacheableData> GetData();
- }
-
- public class DelegateCacheableDataProvider : ICacheableDataProvider
- {
- private readonly Func<Task<string>> _getDataETagDelegate;
- private readonly Func<Task<ICacheableData>> _getDataDelegate;
-
- public DelegateCacheableDataProvider(Func<Task<string>> getDataETagDelegate, Func<Task<ICacheableData>> getDataDelegate)
- {
- _getDataETagDelegate = getDataETagDelegate;
- _getDataDelegate = getDataDelegate;
- }
-
- public Task<ICacheableData> GetData()
- {
- return _getDataDelegate();
- }
-
- public Task<string> GetDataETag()
- {
- return _getDataETagDelegate();
- }
- }
-
- public static class DataCacheHelper
- {
- public static async Task<ActionResult> 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<ILoggerFactory>();
- 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<ActionResult> GenerateActionResult(Controller controller, Func<Task<string>> getDataETagDelegate, Func<Task<ICacheableData>> 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
{
+
/// <summary>
/// Info of a post.
/// </summary>
@@ -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<HttpTimelinePostDataDigest> 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.
/// </summary>
public long Id { get; set; }
- /// <summary>
- /// Content of the post. May be null if post is deleted.
- /// </summary>
- public HttpTimelinePostContent? Content { get; set; }
+ public List<HttpTimelinePostDataDigest> DataList { get; set; } = default!;
/// <summary>
/// True if post is deleted.
/// </summary>
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
-{
- /// <summary>
- /// Info of post content.
- /// </summary>
- public class HttpTimelinePostContent
- {
- public HttpTimelinePostContent() { }
-
- public HttpTimelinePostContent(string type, string? text, string? url, string? eTag)
- {
- Type = type;
- Text = text;
- Url = url;
- ETag = eTag;
- }
-
- /// <summary>
- /// Type of the post content.
- /// </summary>
- public string Type { get; set; } = default!;
- /// <summary>
- /// If post is of text type. This is the text.
- /// </summary>
- public string? Text { get; set; }
- /// <summary>
- /// If post is of image type. This is the image url.
- /// </summary>
- public string? Url { get; set; }
- /// <summary>
- /// If post has data (currently it means it's a image post), this is the data etag.
- /// </summary>
- 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
+ {
+ /// <summary>
+ /// Kind of the data.
+ /// </summary>
+ [Required]
+ [TimelinePostDataKind]
+ public string Kind { get; set; } = default!;
+
+ /// <summary>
+ /// The true data. If kind is text or markdown, this is a string. If kind is image, this is base64 of data.
+ /// </summary>
+ [Required]
+ public string Data { get; set; } = default!;
+ }
+
public class HttpTimelinePostCreateRequest
{
/// <summary>
- /// Content of the new post.
+ /// Data list of the new content.
/// </summary>
[Required]
- public HttpTimelinePostCreateRequestContent Content { get; set; } = default!;
+ [MinLength(1)]
+ public List<HttpTimelinePostCreateRequestData> DataList { get; set; } = default!;
/// <summary>
/// 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
-{
- /// <summary>
- /// Content of post create request.
- /// </summary>
- public class HttpTimelinePostCreateRequestContent
- {
- /// <summary>
- /// Type of post content.
- /// </summary>
- [Required]
- [TimelinePostContentType]
- public string Type { get; set; } = default!;
- /// <summary>
- /// If post is of text type, this is the text.
- /// </summary>
- public string? Text { get; set; }
- /// <summary>
- /// If post is of image type, this is base64 of image data.
- /// </summary>
- 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<HttpTimelinePost> 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<HttpTimelinePostDataDigest> 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<string> AllTypes { get; } = new List<string> { 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; }
/// <summary>If not set, current time is used.</summary>
public DateTime? Time { get; set; }
- public TimelinePostCreateRequestContent Content { get; set; }
+ public List<TimelinePostCreateRequestData> Content { get; set; } = new List<TimelinePostCreateRequestData>();
}
public class TimelinePostPatchRequest
{
public string? Color { get; set; }
public DateTime? Time { get; set; }
- public TimelinePostCreateRequestContent? Content { get; set; }
+ public List<TimelinePostCreateRequestData?>? Content { get; set; }
}
public interface ITimelinePostService
@@ -128,16 +102,7 @@ namespace Timeline.Services /// <exception cref="TimelinePostNotExistException">Thrown when post of <paramref name="postId"/> does not exist or has been deleted.</exception>
Task<TimelinePostEntity> GetPost(long timelineId, long postId, bool includeDelete = false);
- /// <summary>
- /// Get the etag of data of a post.
- /// </summary>
- /// <param name="timelineId">The id of the timeline of the post.</param>
- /// <param name="postId">The id of the post.</param>
- /// <returns>The etag of the data.</returns>
- /// <exception cref="TimelineNotExistException">Thrown when timeline does not exist.</exception>
- /// <exception cref="TimelinePostNotExistException">Thrown when post of <paramref name="postId"/> does not exist or has been deleted.</exception>
- /// <exception cref="TimelinePostNoDataException">Thrown when post has no data.</exception>
- Task<string> GetPostDataETag(long timelineId, long postId);
+ Task<TimelinePostDataDigest> GetPostDataDigest(long timelineId, long postId, long dataIndex);
/// <summary>
/// Get the data of a post.
@@ -148,8 +113,7 @@ namespace Timeline.Services /// <exception cref="TimelineNotExistException">Thrown when timeline does not exist.</exception>
/// <exception cref="TimelinePostNotExistException">Thrown when post of <paramref name="postId"/> does not exist or has been deleted.</exception>
/// <exception cref="TimelinePostNoDataException">Thrown when post has no data.</exception>
- /// <seealso cref="GetPostDataETag(long, long)"/>
- Task<PostData> GetPostData(long timelineId, long postId);
+ Task<TimelinePostData> GetPostData(long timelineId, long postId, long dataIndex);
/// <summary>
/// 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<PostData> GetPostData(long timelineId, long postId)
+ public async Task<TimelinePostData> 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);
- }
- }
-
/// <summary>
/// Provider for default user avatar.
/// </summary>
@@ -42,29 +26,24 @@ namespace Timeline.Services /// Get the etag of default avatar.
/// </summary>
/// <returns></returns>
- Task<string> GetDefaultAvatarETag();
+ Task<ICacheableDataDigest> GetDefaultAvatarETag();
/// <summary>
/// Get the default avatar.
/// </summary>
- Task<AvatarInfo> GetDefaultAvatar();
+ Task<ByteData> GetDefaultAvatar();
}
public interface IUserAvatarService
{
- /// <summary>
- /// Get the etag of a user's avatar. Warning: This method does not check the user existence.
- /// </summary>
- /// <param name="id">The id of the user to get avatar etag of.</param>
- /// <returns>The etag.</returns>
- Task<string> GetAvatarETag(long id);
+ Task<ICacheableDataDigest> GetAvatarDigest(long id);
/// <summary>
/// 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.
/// </summary>
/// <param name="id">The id of the user to get avatar of.</param>
/// <returns>The avatar info.</returns>
- Task<AvatarInfo> GetAvatar(long id);
+ Task<ByteData> GetAvatar(long id);
/// <summary>
/// Set avatar for a user. Warning: This method does not check the user existence.
@@ -74,7 +53,9 @@ namespace Timeline.Services /// <returns>The etag of the avatar.</returns>
/// <exception cref="ArgumentException">Thrown if any field in <paramref name="avatar"/> is null when <paramref name="avatar"/> is not null.</exception>
/// <exception cref="ImageException">Thrown if avatar is of bad format.</exception>
- Task<string> SetAvatar(long id, Avatar? avatar);
+ Task<string> SetAvatar(long id, ByteData avatar);
+
+ Task DeleteAvatar(long id);
}
// TODO! : Make this configurable.
|