aboutsummaryrefslogtreecommitdiff
path: root/BackEnd/Timeline
diff options
context:
space:
mode:
authorcrupest <crupest@outlook.com>2021-02-11 20:10:56 +0800
committercrupest <crupest@outlook.com>2021-02-11 20:10:56 +0800
commitd1317bd9fe08a933a13df88ba692343cde549123 (patch)
treea31dd248ee4f87b0f945c881d68bd6834f7d47c1 /BackEnd/Timeline
parente21b5f85f0d66f51e23a7c1cbf260f2981a83a49 (diff)
downloadtimeline-d1317bd9fe08a933a13df88ba692343cde549123.tar.gz
timeline-d1317bd9fe08a933a13df88ba692343cde549123.tar.bz2
timeline-d1317bd9fe08a933a13df88ba692343cde549123.zip
...
Diffstat (limited to 'BackEnd/Timeline')
-rw-r--r--BackEnd/Timeline/Entities/TimelinePostDataEntity.cs3
-rw-r--r--BackEnd/Timeline/Services/DataManager.cs19
-rw-r--r--BackEnd/Timeline/Services/TimelinePostCreateDataException.cs2
-rw-r--r--BackEnd/Timeline/Services/TimelinePostDataNotExistException.cs10
-rw-r--r--BackEnd/Timeline/Services/TimelinePostService.cs225
5 files changed, 130 insertions, 129 deletions
diff --git a/BackEnd/Timeline/Entities/TimelinePostDataEntity.cs b/BackEnd/Timeline/Entities/TimelinePostDataEntity.cs
index 6ae8fd24..9bc5d3e8 100644
--- a/BackEnd/Timeline/Entities/TimelinePostDataEntity.cs
+++ b/BackEnd/Timeline/Entities/TimelinePostDataEntity.cs
@@ -10,11 +10,12 @@ namespace Timeline.Entities
[Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { get; set; }
+ [Required]
[Column("post")]
public long PostId { get; set; }
[ForeignKey(nameof(PostId))]
- public TimelinePostEntity Timeline { get; set; } = default!;
+ public TimelinePostEntity Post { get; set; } = default!;
[Column("index")]
public long Index { get; set; }
diff --git a/BackEnd/Timeline/Services/DataManager.cs b/BackEnd/Timeline/Services/DataManager.cs
index f24bb59b..b697630c 100644
--- a/BackEnd/Timeline/Services/DataManager.cs
+++ b/BackEnd/Timeline/Services/DataManager.cs
@@ -22,20 +22,22 @@ namespace Timeline.Services
/// increases its ref count and returns a tag to the entry.
/// </summary>
/// <param name="data">The data. Can't be null.</param>
+ /// <param name="saveDatabaseChange">If true save database change. Otherwise it does not save database change.</param>
/// <returns>The tag of the created entry.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="data"/> is null.</exception>
- public Task<string> RetainEntry(byte[] data);
+ public Task<string> RetainEntry(byte[] data, bool saveDatabaseChange = true);
/// <summary>
/// Decrease the the ref count of the entry.
/// Remove it if ref count is zero.
/// </summary>
/// <param name="tag">The tag of the entry.</param>
+ /// <param name="saveDatabaseChange">If true save database change. Otherwise it does not save database change.</param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="tag"/> is null.</exception>
/// <remarks>
/// It's no-op if entry with tag does not exist.
/// </remarks>
- public Task FreeEntry(string tag);
+ public Task FreeEntry(string tag, bool saveDatabaseChange = true);
/// <summary>
/// Retrieve the entry with given tag. If not exist, returns null.
@@ -57,7 +59,7 @@ namespace Timeline.Services
_eTagGenerator = eTagGenerator;
}
- public async Task<string> RetainEntry(byte[] data)
+ public async Task<string> RetainEntry(byte[] data, bool saveDatabaseChange = true)
{
if (data == null)
throw new ArgumentNullException(nameof(data));
@@ -80,11 +82,14 @@ namespace Timeline.Services
{
entity.Ref += 1;
}
- await _database.SaveChangesAsync();
+
+ if (saveDatabaseChange)
+ await _database.SaveChangesAsync();
+
return tag;
}
- public async Task FreeEntry(string tag)
+ public async Task FreeEntry(string tag, bool saveDatabaseChange)
{
if (tag == null)
throw new ArgumentNullException(nameof(tag));
@@ -101,7 +106,9 @@ namespace Timeline.Services
{
entity.Ref -= 1;
}
- await _database.SaveChangesAsync();
+
+ if (saveDatabaseChange)
+ await _database.SaveChangesAsync();
}
}
diff --git a/BackEnd/Timeline/Services/TimelinePostCreateDataException.cs b/BackEnd/Timeline/Services/TimelinePostCreateDataException.cs
index fd1e6664..10a09de7 100644
--- a/BackEnd/Timeline/Services/TimelinePostCreateDataException.cs
+++ b/BackEnd/Timeline/Services/TimelinePostCreateDataException.cs
@@ -6,7 +6,7 @@ namespace Timeline.Services
public TimelinePostCreateDataException() { }
public TimelinePostCreateDataException(string message) : base(message) { }
public TimelinePostCreateDataException(string message, System.Exception inner) : base(message, inner) { }
- public TimelinePostCreateDataException(long index, string? message, System.Exception? inner = null) : base(message, inner) { Index = index; }
+ public TimelinePostCreateDataException(long index, string? message, System.Exception? inner = null) : base($"Data at index {index} is invalid.{(message is null ? "" : " " + message)}", inner) { Index = index; }
protected TimelinePostCreateDataException(
System.Runtime.Serialization.SerializationInfo info,
System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
diff --git a/BackEnd/Timeline/Services/TimelinePostDataNotExistException.cs b/BackEnd/Timeline/Services/TimelinePostDataNotExistException.cs
index eac7a771..c70f5d9c 100644
--- a/BackEnd/Timeline/Services/TimelinePostDataNotExistException.cs
+++ b/BackEnd/Timeline/Services/TimelinePostDataNotExistException.cs
@@ -8,8 +8,18 @@ namespace Timeline.Services
public TimelinePostDataNotExistException() : this(null, null) { }
public TimelinePostDataNotExistException(string? message) : this(message, null) { }
public TimelinePostDataNotExistException(string? message, Exception? inner) : base(message, inner) { }
+ public TimelinePostDataNotExistException(long timelineId, long postId, long dataIndex, string? message = null, Exception? inner = null) : base(message, inner)
+ {
+ TimelineId = timelineId;
+ PostId = postId;
+ DataIndex = dataIndex;
+ }
protected TimelinePostDataNotExistException(
System.Runtime.Serialization.SerializationInfo info,
System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+
+ public long TimelineId { get; set; }
+ public long PostId { get; set; }
+ public long DataIndex { get; set; }
}
}
diff --git a/BackEnd/Timeline/Services/TimelinePostService.cs b/BackEnd/Timeline/Services/TimelinePostService.cs
index cea702a1..8afd0770 100644
--- a/BackEnd/Timeline/Services/TimelinePostService.cs
+++ b/BackEnd/Timeline/Services/TimelinePostService.cs
@@ -4,6 +4,7 @@ using SixLabors.ImageSharp;
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Text;
using System.Threading.Tasks;
using Timeline.Entities;
using Timeline.Helpers;
@@ -62,11 +63,11 @@ namespace Timeline.Services
/// </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 data digest of a post.
@@ -201,7 +202,7 @@ namespace Timeline.Services
if (!includeDeleted)
{
- query = query.Where(p => p.Content != null);
+ query = query.Where(p => !p.Deleted);
}
if (modifiedSince.HasValue)
@@ -214,7 +215,7 @@ namespace Timeline.Services
return await query.ToListAsync();
}
- public async Task<TimelinePostEntity> GetPost(long timelineId, long postId, bool includeDelete = false)
+ public async Task<TimelinePostEntity> GetPost(long timelineId, long postId, bool includeDeleted = false)
{
await CheckTimelineExistence(timelineId);
@@ -225,7 +226,7 @@ namespace Timeline.Services
throw new TimelinePostNotExistException(timelineId, postId, false);
}
- if (!includeDelete && post.Content is null)
+ if (!includeDeleted && post.Deleted)
{
throw new TimelinePostNotExistException(timelineId, postId, true);
}
@@ -233,99 +234,46 @@ namespace Timeline.Services
return post;
}
- public async Task<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 != TimelinePostDataKind.Image)
- throw new TimelinePostNoDataException(ExceptionGetDataNonImagePost);
+ var dataEntity = await _database.TimelinePostData.Where(d => d.PostId == postEntity.Id && d.Index == dataIndex).SingleOrDefaultAsync();
- var tag = postEntity.Content;
+ if (dataEntity is null)
+ throw new TimelinePostDataNotExistException(timelineId, postId, dataIndex);
- return tag;
+ return new CacheableDataDigest(dataEntity.DataTag, dataEntity.LastUpdated);
}
- public async Task<TimelinePostData> 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 != TimelinePostDataKind.Image)
- throw new TimelinePostNoDataException(ExceptionGetDataNonImagePost);
+ var dataEntity = await _database.TimelinePostData.Where(d => d.PostId == postEntity.Id && d.Index == dataIndex).SingleOrDefaultAsync();
- var tag = postEntity.Content;
+ if (dataEntity is null)
+ throw new TimelinePostDataNotExistException(timelineId, postId, dataIndex);
- byte[] data;
+ var data = await _dataManager.GetEntryAndCheck(dataEntity.DataTag, $"Timeline {timelineId}, post {postId}, data {dataIndex} requires this data.");
- try
- {
- data = await _dataManager.GetEntry(tag);
- }
- catch (InvalidOperationException e)
- {
- throw new DatabaseCorruptedException(ExceptionGetDataDataEntryNotExist, e);
- }
-
- if (postEntity.ExtraContent == null)
- {
- _logger.LogWarning(LogGetDataNoFormat);
- var format = Image.DetectFormat(data);
- postEntity.ExtraContent = format.DefaultMimeType;
- await _database.SaveChangesAsync();
- }
-
- return new TimelinePostData
- {
- Data = data,
- Type = postEntity.ExtraContent,
- ETag = tag,
- LastModified = postEntity.LastUpdated
- };
- }
-
- private async Task SaveContent(TimelinePostEntity entity, TimelinePostCreateRequestData content)
- {
- switch (content)
- {
- case TimelinePostCreateRequestTextData c:
- entity.ContentType = c.Kind;
- entity.Content = c.Data;
- break;
- case TimelinePostCreateRequestImageData c:
- var imageFormat = await _imageValidator.Validate(c.Data);
- var imageFormatText = imageFormat.DefaultMimeType;
-
- var tag = await _dataManager.RetainEntry(c.Data);
-
- entity.ContentType = content.Kind;
- entity.Content = tag;
- entity.ExtraContent = imageFormatText;
- break;
- default:
- throw new ArgumentException("Unknown content type.", nameof(content));
- };
- }
-
- private async Task CleanContent(TimelinePostEntity entity)
- {
- if (entity.Content is not null && entity.ContentType == TimelinePostDataKind.Image)
- await _dataManager.FreeEntry(entity.Content);
- entity.Content = null;
+ return new ByteData(data, dataEntity.Kind);
}
public async Task<TimelinePostEntity> CreatePost(long timelineId, long authorId, TimelinePostCreateRequest request)
@@ -333,15 +281,55 @@ namespace Timeline.Services
if (request is null)
throw new ArgumentNullException(nameof(request));
-
- if (request.Content is null)
- throw new ArgumentException("Content is null.", nameof(request));
-
{
if (!_colorValidator.Validate(request.Color, out var message))
throw new ArgumentException("Color is not valid.", nameof(request));
}
+ if (request.DataList is null)
+ throw new ArgumentException("Data list can't be null.", nameof(request));
+
+ if (request.DataList.Count == 0)
+ throw new ArgumentException("Data list can't be empty.", nameof(request));
+
+ if (request.DataList.Count > 100)
+ throw new ArgumentException("Data list count can't be bigger than 100.", nameof(request));
+
+ for (int index = 0; index < request.DataList.Count; index++)
+ {
+ var data = request.DataList[index];
+
+ switch (data.ContentType)
+ {
+ case MimeTypes.ImageGif:
+ case MimeTypes.ImageJpeg:
+ case MimeTypes.ImagePng:
+ case MimeTypes.ImageWebp:
+ try
+ {
+ await _imageValidator.Validate(data.Data, data.ContentType);
+ }
+ catch (ImageException e)
+ {
+ throw new TimelinePostCreateDataException(index, "Image validation failed.", e);
+ }
+ break;
+ case MimeTypes.TextPlain:
+ case MimeTypes.TextMarkdown:
+ try
+ {
+ new UTF8Encoding(false, true).GetString(data.Data);
+ }
+ catch (DecoderFallbackException e)
+ {
+ throw new TimelinePostCreateDataException(index, "Text is not a valid utf-8 sequence.", e);
+ }
+ break;
+ default:
+ throw new TimelinePostCreateDataException(index, "Unsupported content type.");
+ }
+ }
+
request.Time = request.Time?.MyToUtc();
await CheckTimelineExistence(timelineId);
@@ -361,13 +349,29 @@ namespace Timeline.Services
Color = request.Color
};
- await SaveContent(postEntity, request.Content);
-
var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync();
timelineEntity.CurrentPostLocalId += 1;
postEntity.LocalId = timelineEntity.CurrentPostLocalId;
-
_database.TimelinePosts.Add(postEntity);
+ await _database.SaveChangesAsync();
+
+ List<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();
@@ -392,12 +396,10 @@ namespace Timeline.Services
var entity = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync();
- await using var transaction = await _database.Database.BeginTransactionAsync();
-
if (entity is null)
throw new TimelinePostNotExistException(timelineId, postId, false);
- if (entity.Content is null)
+ if (entity.Deleted)
throw new TimelinePostNotExistException(timelineId, postId, true);
if (request.Time.HasValue)
@@ -406,18 +408,10 @@ namespace Timeline.Services
if (request.Color is not null)
entity.Color = request.Color;
- if (request.Content is not null)
- {
- await CleanContent(entity);
- await SaveContent(entity, request.Content);
- }
-
entity.LastUpdated = _clock.GetCurrentTime();
await _database.SaveChangesAsync();
- await transaction.CommitAsync();
-
return entity;
}
@@ -430,15 +424,23 @@ namespace Timeline.Services
if (entity == null)
throw new TimelinePostNotExistException(timelineId, postId, false);
- if (entity.Content == null)
+ if (entity.Deleted)
throw new TimelinePostNotExistException(timelineId, postId, true);
await using var transaction = await _database.Database.BeginTransactionAsync();
- await CleanContent(entity);
-
+ entity.Deleted = true;
entity.LastUpdated = _clock.GetCurrentTime();
+ var dataEntities = await _database.TimelinePostData.Where(d => d.PostId == entity.Id).ToListAsync();
+
+ foreach (var dataEntity in dataEntities)
+ {
+ await _dataManager.FreeEntry(dataEntity.DataTag, false);
+ }
+
+ _database.TimelinePostData.RemoveRange(dataEntities);
+
await _database.SaveChangesAsync();
await transaction.CommitAsync();
@@ -446,30 +448,11 @@ namespace Timeline.Services
public async Task DeleteAllPostsOfUser(long userId)
{
- var posts = await _database.TimelinePosts.Where(p => p.AuthorId == userId).ToListAsync();
-
- var now = _clock.GetCurrentTime();
-
- var dataTags = new List<string>();
-
- foreach (var post in posts)
- {
- if (post.Content != null)
- {
- if (post.ContentType == TimelinePostDataKind.Image)
- {
- dataTags.Add(post.Content);
- }
- post.Content = null;
- }
- post.LastUpdated = now;
- }
-
- await _database.SaveChangesAsync();
+ var postEntities = await _database.TimelinePosts.Where(p => p.AuthorId == userId).Select(p => new { p.TimelineId, p.LocalId }).ToListAsync();
- foreach (var dataTag in dataTags)
+ foreach (var postEntity in postEntities)
{
- await _dataManager.FreeEntry(dataTag);
+ await this.DeletePost(postEntity.TimelineId, postEntity.LocalId);
}
}
@@ -479,9 +462,9 @@ namespace Timeline.Services
var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync();
- var postEntity = await _database.TimelinePosts.Where(p => p.Id == postId).Select(p => new { p.Content, p.AuthorId }).SingleOrDefaultAsync();
+ var postEntity = await _database.TimelinePosts.Where(p => p.Id == postId).Select(p => new { p.Deleted, p.AuthorId }).SingleOrDefaultAsync();
- if (postEntity == null)
+ if (postEntity is null)
{
if (throwOnPostNotExist)
throw new TimelinePostNotExistException(timelineId, postId, false);
@@ -489,7 +472,7 @@ namespace Timeline.Services
return true;
}
- if (postEntity.Content == null && throwOnPostNotExist)
+ if (postEntity.Deleted && throwOnPostNotExist)
{
throw new TimelinePostNotExistException(timelineId, postId, true);
}