From 968140e8aaba398e10585e978aff33d7b32e824a Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 6 Mar 2020 22:28:32 +0800 Subject: Init development of post image feature. --- Timeline/Services/AvatarFormatException.cs | 51 ---------------------------- Timeline/Services/ImageException.cs | 54 ++++++++++++++++++++++++++++++ Timeline/Services/ImageValidator.cs | 50 +++++++++++++++++++++++++++ Timeline/Services/TimelineService.cs | 39 ++++++++++++++++----- Timeline/Services/UserAvatarService.cs | 24 ++++--------- 5 files changed, 141 insertions(+), 77 deletions(-) delete mode 100644 Timeline/Services/AvatarFormatException.cs create mode 100644 Timeline/Services/ImageException.cs create mode 100644 Timeline/Services/ImageValidator.cs (limited to 'Timeline/Services') diff --git a/Timeline/Services/AvatarFormatException.cs b/Timeline/Services/AvatarFormatException.cs deleted file mode 100644 index 788eabb2..00000000 --- a/Timeline/Services/AvatarFormatException.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Globalization; - -namespace Timeline.Services -{ - /// - /// Thrown when avatar is of bad format. - /// - [Serializable] - public class AvatarFormatException : Exception - { - public enum ErrorReason - { - /// - /// Decoding image failed. - /// - CantDecode, - /// - /// Decoding succeeded but the real type is not the specified type. - /// - UnmatchedFormat, - /// - /// Image is not a square. - /// - BadSize - } - - public AvatarFormatException() : base(MakeMessage(null)) { } - public AvatarFormatException(string message) : base(message) { } - public AvatarFormatException(string message, Exception inner) : base(message, inner) { } - - public AvatarFormatException(Avatar avatar, ErrorReason error) : base(MakeMessage(error)) { Avatar = avatar; Error = error; } - public AvatarFormatException(Avatar avatar, ErrorReason error, Exception inner) : base(MakeMessage(error), inner) { Avatar = avatar; Error = error; } - - protected AvatarFormatException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - - private static string MakeMessage(ErrorReason? reason) => - string.Format(CultureInfo.InvariantCulture, Resources.Services.Exception.AvatarFormatException, reason switch - { - ErrorReason.CantDecode => Resources.Services.Exception.AvatarFormatExceptionCantDecode, - ErrorReason.UnmatchedFormat => Resources.Services.Exception.AvatarFormatExceptionUnmatchedFormat, - ErrorReason.BadSize => Resources.Services.Exception.AvatarFormatExceptionBadSize, - _ => Resources.Services.Exception.AvatarFormatExceptionUnknownError - }); - - public ErrorReason? Error { get; set; } - public Avatar? Avatar { get; set; } - } -} diff --git a/Timeline/Services/ImageException.cs b/Timeline/Services/ImageException.cs new file mode 100644 index 00000000..c6126aa3 --- /dev/null +++ b/Timeline/Services/ImageException.cs @@ -0,0 +1,54 @@ +using System; +using System.Globalization; + +namespace Timeline.Services +{ + [Serializable] + public class ImageException : Exception + { + public enum ErrorReason + { + /// + /// Decoding image failed. + /// + CantDecode, + /// + /// Decoding succeeded but the real type is not the specified type. + /// + UnmatchedFormat, + /// + /// Image is not a square. + /// + NotSquare + } + + public ImageException() : base(MakeMessage(null)) { } + public ImageException(string message) : base(message) { } + public ImageException(string message, Exception inner) : base(message, inner) { } + + public ImageException(ErrorReason error, byte[]? data = null, string? requestType = null, string? realType = null) : base(MakeMessage(error)) { Error = error; ImageData = data; RequestType = requestType; RealType = realType; } + public ImageException(Exception inner, ErrorReason error, byte[]? data = null, string? requestType = null, string? realType = null) : base(MakeMessage(error), inner) { Error = error; ImageData = data; RequestType = requestType; RealType = realType; } + + protected ImageException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + private static string MakeMessage(ErrorReason? reason) => + string.Format(CultureInfo.InvariantCulture, Resources.Services.Exception.ImageException, reason switch + { + ErrorReason.CantDecode => Resources.Services.Exception.ImageExceptionCantDecode, + ErrorReason.UnmatchedFormat => Resources.Services.Exception.ImageExceptionUnmatchedFormat, + ErrorReason.NotSquare => Resources.Services.Exception.ImageExceptionBadSize, + _ => Resources.Services.Exception.ImageExceptionUnknownError + }); + + public ErrorReason? Error { get; } +#pragma warning disable CA1819 // Properties should not return arrays + public byte[]? ImageData { get; } +#pragma warning restore CA1819 // Properties should not return arrays + public string? RequestType { get; } + + // This field will be null if decoding failed. + public string? RealType { get; } + } +} diff --git a/Timeline/Services/ImageValidator.cs b/Timeline/Services/ImageValidator.cs new file mode 100644 index 00000000..897a37b8 --- /dev/null +++ b/Timeline/Services/ImageValidator.cs @@ -0,0 +1,50 @@ +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace Timeline.Services +{ + public class ImageValidator + { + private readonly bool _requireSquare; + + public ImageValidator(bool requireSquare = false) + { + _requireSquare = requireSquare; + } + + /// + /// Validate a image data. + /// + /// The data of the image. Can't be null. + /// If not null, the real image format will be check against the requested format and throw if not match. If null, then do not check. + /// The format. + /// Thrown when is null. + /// Thrown when image data can't be decoded or real type does not match request type or image is not square when required. + public async Task Validate(byte[] data, string? requestType = null) + { + if (data == null) + throw new ArgumentNullException(nameof(data)); + + var format = await Task.Run(() => + { + try + { + using var image = Image.Load(data, out IImageFormat format); + if (requestType != null && !format.MimeTypes.Contains(requestType)) + throw new ImageException(ImageException.ErrorReason.UnmatchedFormat, data, requestType, format.DefaultMimeType); + if (_requireSquare && image.Width != image.Height) + throw new ImageException(ImageException.ErrorReason.NotSquare, data, requestType, format.DefaultMimeType); + return format; + } + catch (UnknownImageFormatException e) + { + throw new ImageException(e, ImageException.ErrorReason.CantDecode, data, requestType, null); + } + }); + return format; + } + } +} diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs index 379ec8f5..d999587a 100644 --- a/Timeline/Services/TimelineService.cs +++ b/Timeline/Services/TimelineService.cs @@ -90,14 +90,14 @@ namespace Timeline.Services Task> GetPosts(string name); /// - /// Create a new post in timeline. + /// Create a new text post in timeline. /// /// Username or the timeline name. See remarks of . /// The author's id. - /// The content. + /// The content text. /// The time of the post. If null, then use current time. /// The info of the created post. - /// Thrown when or is null. + /// Thrown when or is null. /// Thrown when is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service). /// /// Thrown when timeline does not exist. @@ -106,7 +106,27 @@ namespace Timeline.Services /// and the inner exception should be a . /// /// Thrown if user with does not exist. - Task CreatePost(string name, long authorId, string content, DateTime? time); + Task CreateTextPost(string name, long authorId, string text, DateTime? time); + + /// + /// Create a new image post in timeline. + /// + /// Username or the timeline name. See remarks of . + /// The author's id. + /// The image data. + /// The time of the post. If null, then use current time. + /// The info of the created post. + /// Thrown when or is null. + /// Thrown when is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service). + /// + /// Thrown when timeline does not exist. + /// For normal timeline, it means the name does not exist. + /// For personal timeline, it means the user of that username does not exist + /// and the inner exception should be a . + /// + /// Thrown if user with does not exist. + /// Thrown if data is not a image. Validated by . + Task CreateImagePost(string name, long authorId, byte[] data, DateTime? time); /// /// Delete a post @@ -398,12 +418,12 @@ namespace Timeline.Services return posts; } - public async Task CreatePost(string name, long authorId, string content, DateTime? time) + public async Task CreateTextPost(string name, long authorId, string text, DateTime? time) { if (name == null) throw new ArgumentNullException(nameof(name)); - if (content == null) - throw new ArgumentNullException(nameof(content)); + if (text == null) + throw new ArgumentNullException(nameof(text)); var timelineId = await FindTimelineId(name); var timelineEntity = await Database.Timelines.Where(t => t.Id == timelineId).SingleAsync(); @@ -418,7 +438,8 @@ namespace Timeline.Services var postEntity = new TimelinePostEntity { LocalId = timelineEntity.CurrentPostLocalId, - Content = content, + ContentType = TimelinePostContentTypes.Text, + Content = text, AuthorId = authorId, TimelineId = timelineId, Time = finalTime, @@ -430,7 +451,7 @@ namespace Timeline.Services return new TimelinePostInfo { Id = postEntity.LocalId, - Content = content, + Content = text, Author = author, Time = finalTime, LastUpdated = currentTime diff --git a/Timeline/Services/UserAvatarService.cs b/Timeline/Services/UserAvatarService.cs index 52d079a3..27922bab 100644 --- a/Timeline/Services/UserAvatarService.cs +++ b/Timeline/Services/UserAvatarService.cs @@ -53,7 +53,7 @@ namespace Timeline.Services /// Validate a avatar's format and size info. /// /// The avatar to validate. - /// Thrown when validation failed. + /// Thrown when validation failed. Task Validate(Avatar avatar); } @@ -79,7 +79,7 @@ namespace Timeline.Services /// The id of the user to set avatar for. /// The avatar. Can be null to delete the saved avatar. /// Thrown if any field in is null when is not null. - /// Thrown if avatar is of bad format. + /// Thrown if avatar is of bad format. Task SetAvatar(long id, Avatar? avatar); } @@ -134,23 +134,13 @@ namespace Timeline.Services public class UserAvatarValidator : IUserAvatarValidator { + private readonly ImageValidator _innerValidator = new ImageValidator(true); + public Task Validate(Avatar avatar) { - return Task.Run(() => - { - try - { - using var image = Image.Load(avatar.Data, out IImageFormat format); - if (!format.MimeTypes.Contains(avatar.Type)) - throw new AvatarFormatException(avatar, AvatarFormatException.ErrorReason.UnmatchedFormat); - if (image.Width != image.Height) - throw new AvatarFormatException(avatar, AvatarFormatException.ErrorReason.BadSize); - } - catch (UnknownImageFormatException e) - { - throw new AvatarFormatException(avatar, AvatarFormatException.ErrorReason.CantDecode, e); - } - }); + if (avatar == null) + throw new ArgumentNullException(nameof(avatar)); + return _innerValidator.Validate(avatar.Data, avatar.Type); } } -- cgit v1.2.3