From 36a5a7c81569bbc4fa76b77e9823767d951944b4 Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Sun, 18 Aug 2019 18:07:50 +0800 Subject: Add avatar service. --- Timeline/Entities/DatabaseContext.cs | 3 + Timeline/Entities/UserAvatar.cs | 18 ++++ Timeline/Services/UserAvatarService.cs | 181 +++++++++++++++++++++++++++++++++ Timeline/Timeline.csproj | 6 -- Timeline/appsettings.Development.json | 2 +- Timeline/default-avatar.png | Bin 0 -> 26442 bytes 6 files changed, 203 insertions(+), 7 deletions(-) create mode 100644 Timeline/Entities/UserAvatar.cs create mode 100644 Timeline/Services/UserAvatarService.cs create mode 100644 Timeline/default-avatar.png (limited to 'Timeline') diff --git a/Timeline/Entities/DatabaseContext.cs b/Timeline/Entities/DatabaseContext.cs index 3629e821..f32e5992 100644 --- a/Timeline/Entities/DatabaseContext.cs +++ b/Timeline/Entities/DatabaseContext.cs @@ -27,6 +27,8 @@ namespace Timeline.Entities [Column("version"), Required] public long Version { get; set; } + + public UserAvatar Avatar { get; set; } } public class DatabaseContext : DbContext @@ -44,5 +46,6 @@ namespace Timeline.Entities } public DbSet Users { get; set; } + public DbSet UserAvatars { get; set; } } } diff --git a/Timeline/Entities/UserAvatar.cs b/Timeline/Entities/UserAvatar.cs new file mode 100644 index 00000000..d7c24403 --- /dev/null +++ b/Timeline/Entities/UserAvatar.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Timeline.Entities +{ + [Table("user_avatars")] + public class UserAvatar + { + [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + + [Column("data"), Required] + public byte[] Data { get; set; } + + [Column("type"), Required] + public string Type { get; set; } + } +} diff --git a/Timeline/Services/UserAvatarService.cs b/Timeline/Services/UserAvatarService.cs new file mode 100644 index 00000000..21153575 --- /dev/null +++ b/Timeline/Services/UserAvatarService.cs @@ -0,0 +1,181 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Timeline.Entities; + +namespace Timeline.Services +{ + public class Avatar + { + public string Type { get; set; } + public byte[] Data { get; set; } + } + + /// + /// Thrown when avatar is of bad format. + /// + [Serializable] + public class AvatarDataException : Exception + { + public AvatarDataException(Avatar avatar, string message) : base(message) { Avatar = avatar; } + public AvatarDataException(Avatar avatar, string message, Exception inner) : base(message, inner) { Avatar = avatar; } + protected AvatarDataException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + public Avatar Avatar { get; set; } + } + + /// + /// Provider for default user avatar. + /// + /// + /// Mainly for unit tests. + /// + public interface IDefaultUserAvatarProvider + { + /// + /// Get the default avatar. + /// + Task GetDefaultAvatar(); + } + + public interface IUserAvatarService + { + /// + /// Get avatar of a user. If the user has no avatar, a default one is returned. + /// + /// The username of the user to get avatar of. + /// The avatar. + /// Thrown if is null or empty. + /// Thrown if the user does not exist. + Task GetAvatar(string username); + + /// + /// Set avatar for a user. + /// + /// The username of the user to set avatar for. + /// The avatar. Can be null to delete the saved avatar. + /// Throw if is null or empty. + /// Or thrown if is not null but is null or empty or is null. + /// Thrown if the user does not exist. + /// Thrown if avatar is of bad format. + Task SetAvatar(string username, Avatar avatar); + } + + public class DefaultUserAvatarProvider : IDefaultUserAvatarProvider + { + private readonly IHostingEnvironment _environment; + + public DefaultUserAvatarProvider(IHostingEnvironment environment) + { + _environment = environment; + } + + public async Task GetDefaultAvatar() + { + return new Avatar + { + Type = "image/png", + Data = await File.ReadAllBytesAsync(Path.Combine(_environment.ContentRootPath, "default-avatar.png")) + }; + } + } + + public class UserAvatarService : IUserAvatarService + { + + private readonly ILogger _logger; + + private readonly DatabaseContext _database; + + private readonly IDefaultUserAvatarProvider _defaultUserAvatarProvider; + + public UserAvatarService(ILogger logger, DatabaseContext database, IDefaultUserAvatarProvider defaultUserAvatarProvider) + { + _logger = logger; + _database = database; + _defaultUserAvatarProvider = defaultUserAvatarProvider; + } + + public async Task GetAvatar(string username) + { + if (string.IsNullOrEmpty(username)) + throw new ArgumentException("Username is null or empty.", nameof(username)); + + var user = await _database.Users.Where(u => u.Name == username).SingleOrDefaultAsync(); + if (user == null) + throw new UserNotExistException(username); + + await _database.Entry(user).Reference(u => u.Avatar).LoadAsync(); + var avatar = user.Avatar; + + if (avatar == null) + { + return await _defaultUserAvatarProvider.GetDefaultAvatar(); + } + else + { + return new Avatar + { + Type = avatar.Type, + Data = avatar.Data + }; + } + } + + public async Task SetAvatar(string username, Avatar avatar) + { + if (string.IsNullOrEmpty(username)) + throw new ArgumentException("Username is null or empty.", nameof(username)); + + if (avatar != null) + { + if (string.IsNullOrEmpty(avatar.Type)) + throw new ArgumentException("Type of avatar is null or empty.", nameof(avatar)); + if (avatar.Data == null) + throw new ArgumentException("Data of avatar is null.", nameof(avatar)); + } + + var user = await _database.Users.Where(u => u.Name == username).SingleOrDefaultAsync(); + if (user == null) + throw new UserNotExistException(username); + + await _database.Entry(user).Reference(u => u.Avatar).LoadAsync(); + var avatarEntity = user.Avatar; + + if (avatar == null) + { + if (avatarEntity == null) + return; + else + { + _database.UserAvatars.Remove(avatarEntity); + await _database.SaveChangesAsync(); + } + } + else + { + // TODO: Use image library to check the format to prohibit bad data. + if (avatarEntity == null) + { + user.Avatar = new UserAvatar + { + Type = avatar.Type, + Data = avatar.Data + }; + } + else + { + avatarEntity.Type = avatar.Type; + avatarEntity.Data = avatar.Data; + } + await _database.SaveChangesAsync(); + } + } + } +} diff --git a/Timeline/Timeline.csproj b/Timeline/Timeline.csproj index 1f70c634..29ff3354 100644 --- a/Timeline/Timeline.csproj +++ b/Timeline/Timeline.csproj @@ -5,12 +5,6 @@ 1f6fb74d-4277-4bc0-aeea-b1fc5ffb0b43 crupest - - - - - - diff --git a/Timeline/appsettings.Development.json b/Timeline/appsettings.Development.json index db4b074a..424b3885 100644 --- a/Timeline/appsettings.Development.json +++ b/Timeline/appsettings.Development.json @@ -7,6 +7,6 @@ } }, "JwtConfig": { - "SigningKey": "crupest hahahahahahahhahahahahaha" + "SigningKey": "this is very very very very very long secret" } } diff --git a/Timeline/default-avatar.png b/Timeline/default-avatar.png new file mode 100644 index 00000000..4086e1d2 Binary files /dev/null and b/Timeline/default-avatar.png differ -- cgit v1.2.3