using Microsoft.AspNetCore.Hosting; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats; 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 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 AvatarDataException(Avatar avatar, ErrorReason error, string message) : base(message) { Avatar = avatar; Error = error; } public AvatarDataException(Avatar avatar, ErrorReason error, string message, Exception inner) : base(message, inner) { Avatar = avatar; Error = error; } protected AvatarDataException( System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) : base(info, context) { } public ErrorReason Error { get; set; } 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 IUserAvatarValidator { /// /// Validate a avatar's format and size info. /// /// The avatar to validate. /// Thrown when validation failed. Task Validate(Avatar avatar); } 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 UserAvatarValidator : IUserAvatarValidator { 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 AvatarDataException(avatar, AvatarDataException.ErrorReason.UnmatchedFormat, "Image's actual mime type is not the specified one."); if (image.Width != image.Height) throw new AvatarDataException(avatar, AvatarDataException.ErrorReason.BadSize, "Image is not a square, aka, width is not equal to height."); } } catch (UnknownImageFormatException e) { throw new AvatarDataException(avatar, AvatarDataException.ErrorReason.CantDecode, "Failed to decode image. See inner exception.", e); } }); } } public class UserAvatarService : IUserAvatarService { private readonly ILogger _logger; private readonly DatabaseContext _database; private readonly IDefaultUserAvatarProvider _defaultUserAvatarProvider; private readonly IUserAvatarValidator _avatarValidator; public UserAvatarService(ILogger logger, DatabaseContext database, IDefaultUserAvatarProvider defaultUserAvatarProvider, IUserAvatarValidator avatarValidator) { _logger = logger; _database = database; _defaultUserAvatarProvider = defaultUserAvatarProvider; _avatarValidator = avatarValidator; } 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(); _logger.LogInformation("Removed an entry in user_avatars."); } } else { await _avatarValidator.Validate(avatar); 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(); _logger.LogInformation("Added or modified an entry in user_avatars."); } } } public static class UserAvatarServiceCollectionExtensions { public static void AddUserAvatarService(this IServiceCollection services) { services.AddScoped(); services.AddSingleton(); services.AddSingleton(); } } }