diff options
author | crupest <crupest@outlook.com> | 2021-02-12 22:39:57 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-02-12 22:39:57 +0800 |
commit | 94eba6cda12010c8657f0e7c5e6977a8d3b46e8c (patch) | |
tree | d6638f60df77ed97fc70f1750f90019c2bb9e9c1 /BackEnd/Timeline/Services/TimelinePostService.cs | |
parent | a759460ad757922e761a07504bcdea7eeaa07860 (diff) | |
parent | 83910122bfd0aa9bd207b6d5f631774415312716 (diff) | |
download | timeline-94eba6cda12010c8657f0e7c5e6977a8d3b46e8c.tar.gz timeline-94eba6cda12010c8657f0e7c5e6977a8d3b46e8c.tar.bz2 timeline-94eba6cda12010c8657f0e7c5e6977a8d3b46e8c.zip |
Merge pull request #267 from crupest/backend
春节大换血 Spring festival big change.
Diffstat (limited to 'BackEnd/Timeline/Services/TimelinePostService.cs')
-rw-r--r-- | BackEnd/Timeline/Services/TimelinePostService.cs | 331 |
1 files changed, 127 insertions, 204 deletions
diff --git a/BackEnd/Timeline/Services/TimelinePostService.cs b/BackEnd/Timeline/Services/TimelinePostService.cs index 66ec8090..62bc43cc 100644 --- a/BackEnd/Timeline/Services/TimelinePostService.cs +++ b/BackEnd/Timeline/Services/TimelinePostService.cs @@ -1,108 +1,49 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
-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;
+using Timeline.Helpers.Cache;
using Timeline.Models;
using Timeline.Models.Validation;
using Timeline.Services.Exceptions;
-using static Timeline.Resources.Services.TimelineService;
namespace Timeline.Services
{
- public class PostData : ICacheableData
+ public class TimelinePostCreateRequestData
{
-#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 abstract class TimelinePostCreateRequestContent
- {
- public abstract string TypeName { get; }
- }
-
- public class TimelinePostCreateRequestTextContent : TimelinePostCreateRequestContent
- {
- private string _text;
-
- public TimelinePostCreateRequestTextContent(string text)
- {
- if (text is null)
- throw new ArgumentNullException(nameof(text));
-
- _text = text;
- }
-
- public override string TypeName => TimelinePostContentTypes.Text;
-
- public string Text
+ public TimelinePostCreateRequestData(string contentType, byte[] data)
{
- get => _text;
- set
- {
- if (value is null)
- throw new ArgumentNullException(nameof(value));
- _text = value;
- }
+ ContentType = contentType;
+ Data = data;
}
- }
-
- public class TimelinePostCreateRequestImageContent : TimelinePostCreateRequestContent
- {
- private byte[] _data;
-
- public TimelinePostCreateRequestImageContent(byte[] data)
- {
- if (data is null)
- throw new ArgumentNullException(nameof(data));
-
- _data = data;
- }
-
- public override string TypeName => TimelinePostContentTypes.Image;
+ public string ContentType { 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; }
+#pragma warning disable CA2227
+ public List<TimelinePostCreateRequestData> DataList { get; set; } = new List<TimelinePostCreateRequestData>();
+#pragma warning restore CA2227
}
public class TimelinePostPatchRequest
{
public string? Color { get; set; }
public DateTime? Time { get; set; }
- public TimelinePostCreateRequestContent? Content { get; set; }
}
public interface ITimelinePostService
@@ -122,34 +63,35 @@ namespace Timeline.Services /// </summary>
/// <param name="timelineId">The id of the timeline of the post.</param>
/// <param name="postId">The id of the post.</param>
- /// <param name="includeDelete">If true, return the entity even if it is deleted.</param>
+ /// <param name="includeDeleted">If true, return the entity even if it is deleted.</param>
/// <returns>The post.</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>
- Task<TimelinePostEntity> GetPost(long timelineId, long postId, bool includeDelete = false);
+ Task<TimelinePostEntity> GetPost(long timelineId, long postId, bool includeDeleted = false);
/// <summary>
- /// Get the etag of data of a post.
+ /// Get the data digest 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>
+ /// <param name="timelineId">The timeline id.</param>
+ /// <param name="postId">The post id.</param>
+ /// <param name="dataIndex">The index of the data.</param>
+ /// <returns>The data digest.</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);
+ /// <exception cref="TimelinePostDataNotExistException">Thrown when data of that index does not exist.</exception>
+ Task<ICacheableDataDigest> GetPostDataDigest(long timelineId, long postId, long dataIndex);
/// <summary>
/// Get the 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>
+ /// <param name="timelineId">The timeline id.</param>
+ /// <param name="postId">The post id.</param>
+ /// <param name="dataIndex">The index of the data.</param>
+ /// <returns>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>
- /// <seealso cref="GetPostDataETag(long, long)"/>
- Task<PostData> GetPostData(long timelineId, long postId);
+ /// <exception cref="TimelinePostDataNotExistException">Thrown when data of that index does not exist.</exception>
+ Task<ByteData> GetPostData(long timelineId, long postId, long dataIndex);
/// <summary>
/// Create a new post in timeline.
@@ -176,7 +118,6 @@ namespace Timeline.Services /// <exception cref="ArgumentException">Thrown when <paramref name="request"/> is of invalid format.</exception>
/// <exception cref="TimelineNotExistException">Thrown when timeline does not exist.</exception>
/// <exception cref="TimelinePostNotExistException">Thrown when post does not exist.</exception>
- /// <exception cref="ImageException">Thrown if data is not a image. Validated by <see cref="ImageValidator"/>.</exception>
Task<TimelinePostEntity> PatchPost(long timelineId, long postId, TimelinePostPatchRequest request);
/// <summary>
@@ -261,7 +202,7 @@ namespace Timeline.Services if (!includeDeleted)
{
- query = query.Where(p => p.Content != null);
+ query = query.Where(p => !p.Deleted);
}
if (modifiedSince.HasValue)
@@ -274,7 +215,7 @@ namespace Timeline.Services return await query.ToListAsync();
}
- public async Task<TimelinePostEntity> GetPost(long timelineId, long postId, bool includeDelete = false)
+ public async Task<TimelinePostEntity> GetPost(long timelineId, long postId, bool includeDeleted = false)
{
await CheckTimelineExistence(timelineId);
@@ -285,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);
}
@@ -293,99 +234,46 @@ namespace Timeline.Services return post;
}
- public async Task<string> GetPostDataETag(long timelineId, long postId)
+ public async Task<ICacheableDataDigest> 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 != TimelinePostContentTypes.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<PostData> GetPostData(long timelineId, long postId)
+ public async Task<ByteData> 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 != TimelinePostContentTypes.Image)
- throw new TimelinePostNoDataException(ExceptionGetDataNonImagePost);
-
- var tag = postEntity.Content;
-
- byte[] data;
+ var dataEntity = await _database.TimelinePostData.Where(d => d.PostId == postEntity.Id && d.Index == dataIndex).SingleOrDefaultAsync();
- try
- {
- data = await _dataManager.GetEntry(tag);
- }
- catch (InvalidOperationException e)
- {
- throw new DatabaseCorruptedException(ExceptionGetDataDataEntryNotExist, e);
- }
+ if (dataEntity is null)
+ throw new TimelinePostDataNotExistException(timelineId, postId, dataIndex);
- if (postEntity.ExtraContent == null)
- {
- _logger.LogWarning(LogGetDataNoFormat);
- var format = Image.DetectFormat(data);
- postEntity.ExtraContent = format.DefaultMimeType;
- await _database.SaveChangesAsync();
- }
+ var data = await _dataManager.GetEntryAndCheck(dataEntity.DataTag, $"Timeline {timelineId}, post {postId}, data {dataIndex} requires this data.");
- return new PostData
- {
- Data = data,
- Type = postEntity.ExtraContent,
- ETag = tag,
- LastModified = postEntity.LastUpdated
- };
- }
-
- private async Task SaveContent(TimelinePostEntity entity, TimelinePostCreateRequestContent content)
- {
- switch (content)
- {
- case TimelinePostCreateRequestTextContent c:
- entity.ContentType = c.TypeName;
- entity.Content = c.Text;
- break;
- case TimelinePostCreateRequestImageContent c:
- var imageFormat = await _imageValidator.Validate(c.Data);
- var imageFormatText = imageFormat.DefaultMimeType;
-
- var tag = await _dataManager.RetainEntry(c.Data);
-
- entity.ContentType = content.TypeName;
- 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 == TimelinePostContentTypes.Image)
- await _dataManager.FreeEntry(entity.Content);
- entity.Content = null;
+ return new ByteData(data, dataEntity.Kind);
}
public async Task<TimelinePostEntity> CreatePost(long timelineId, long authorId, TimelinePostCreateRequest request)
@@ -393,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);
@@ -421,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<string> dataTags = new List<string>();
+
+ 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();
@@ -452,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)
@@ -466,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;
}
@@ -490,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();
@@ -506,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<string>();
-
- foreach (var post in posts)
- {
- if (post.Content != null)
- {
- if (post.ContentType == TimelinePostContentTypes.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);
}
}
@@ -539,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);
@@ -549,7 +472,7 @@ namespace Timeline.Services return true;
}
- if (postEntity.Content == null && throwOnPostNotExist)
+ if (postEntity.Deleted && throwOnPostNotExist)
{
throw new TimelinePostNotExistException(timelineId, postId, true);
}
|