From c324d1dad0ffc1a1013b22792078415e7a50c470 Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Thu, 24 Oct 2019 16:56:41 +0800 Subject: ... --- Timeline/Services/UserAvatarService.cs | 187 ++++++++++++++++----------------- 1 file changed, 89 insertions(+), 98 deletions(-) (limited to 'Timeline/Services/UserAvatarService.cs') diff --git a/Timeline/Services/UserAvatarService.cs b/Timeline/Services/UserAvatarService.cs index ecec5a31..4c65a0fa 100644 --- a/Timeline/Services/UserAvatarService.cs +++ b/Timeline/Services/UserAvatarService.cs @@ -10,53 +10,24 @@ using System.IO; using System.Linq; using System.Threading.Tasks; using Timeline.Entities; +using Timeline.Helpers; +using Timeline.Models.Validation; namespace Timeline.Services { public class Avatar { - public string Type { get; set; } - public byte[] Data { get; set; } + public string Type { get; set; } = default!; + [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1819:Properties should not return arrays", Justification = "DTO Object")] + public byte[] Data { get; set; } = default!; } public class AvatarInfo { - public Avatar Avatar { get; set; } + public Avatar Avatar { get; set; } = default!; public DateTime LastModified { 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. /// @@ -83,7 +54,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); } @@ -94,16 +65,18 @@ namespace Timeline.Services /// /// The username of the user to get avatar etag of. /// The etag. - /// Thrown if is null or empty. + /// Thrown if is null. + /// Thrown if the is of bad format. /// Thrown if the user does not exist. Task GetAvatarETag(string username); /// - /// Get avatar of a user. If the user has no avatar, a default one is returned. + /// Get avatar of a user. If the user has no avatar set, a default one is returned. /// /// The username of the user to get avatar of. /// The avatar info. - /// Thrown if is null or empty. + /// Thrown if is null. + /// Thrown if the is of bad format. /// Thrown if the user does not exist. Task GetAvatar(string username); @@ -112,38 +85,41 @@ namespace Timeline.Services /// /// 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. + /// Throw if is null. + /// Thrown if any field in is null when is not null. + /// Thrown if the is of bad format. /// Thrown if the user does not exist. - /// Thrown if avatar is of bad format. - Task SetAvatar(string username, Avatar avatar); + /// Thrown if avatar is of bad format. + Task SetAvatar(string username, Avatar? avatar); } + // TODO! : Make this configurable. public class DefaultUserAvatarProvider : IDefaultUserAvatarProvider { - private readonly IWebHostEnvironment _environment; - private readonly IETagGenerator _eTagGenerator; - private byte[] _cacheData; + private readonly string _avatarPath; + + private byte[] _cacheData = default!; private DateTime _cacheLastModified; - private string _cacheETag; + private string _cacheETag = default!; + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "DI.")] public DefaultUserAvatarProvider(IWebHostEnvironment environment, IETagGenerator eTagGenerator) { - _environment = environment; + _avatarPath = Path.Combine(environment.ContentRootPath, "default-avatar.png"); _eTagGenerator = eTagGenerator; } private async Task CheckAndInit() { - if (_cacheData != null) - return; - - var path = Path.Combine(_environment.ContentRootPath, "default-avatar.png"); - _cacheData = await File.ReadAllBytesAsync(path); - _cacheLastModified = File.GetLastWriteTime(path); - _cacheETag = _eTagGenerator.Generate(_cacheData); + var path = _avatarPath; + if (_cacheData == null || File.GetLastWriteTime(path) > _cacheLastModified) + { + _cacheData = await File.ReadAllBytesAsync(path); + _cacheLastModified = File.GetLastWriteTime(path); + _cacheETag = _eTagGenerator.Generate(_cacheData); + } } public async Task GetDefaultAvatarETag() @@ -175,17 +151,15 @@ namespace Timeline.Services { 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."); - } + 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 AvatarDataException(avatar, AvatarDataException.ErrorReason.CantDecode, "Failed to decode image. See inner exception.", e); + throw new AvatarFormatException(avatar, AvatarFormatException.ErrorReason.CantDecode, e); } }); } @@ -203,6 +177,8 @@ namespace Timeline.Services private readonly IETagGenerator _eTagGenerator; + private readonly UsernameValidator _usernameValidator; + public UserAvatarService( ILogger logger, DatabaseContext database, @@ -215,13 +191,14 @@ namespace Timeline.Services _defaultUserAvatarProvider = defaultUserAvatarProvider; _avatarValidator = avatarValidator; _eTagGenerator = eTagGenerator; + _usernameValidator = new UsernameValidator(); } public async Task GetAvatarETag(string username) { - var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username); + var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, _usernameValidator, username); - var eTag = (await _database.UserAvatars.Where(a => a.UserId == userId).Select(a => new { a.ETag }).SingleAsync()).ETag; + var eTag = (await _database.UserAvatars.Where(a => a.UserId == userId).Select(a => new { a.ETag }).SingleOrDefaultAsync())?.ETag; if (eTag == null) return await _defaultUserAvatarProvider.GetDefaultAvatarETag(); else @@ -230,54 +207,57 @@ namespace Timeline.Services public async Task GetAvatar(string username) { - var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username); + var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, _usernameValidator, username); - var avatar = await _database.UserAvatars.Where(a => a.UserId == userId).Select(a => new { a.Type, a.Data, a.LastModified }).SingleAsync(); + var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == userId).Select(a => new { a.Type, a.Data, a.LastModified }).SingleOrDefaultAsync(); - if ((avatar.Type == null) != (avatar.Data == null)) + if (avatarEntity != null) { - _logger.LogCritical("Database corupted! One of type and data of a avatar is null but the other is not."); - throw new DatabaseCorruptedException(); - } + if (!LanguageHelper.AreSame(avatarEntity.Data == null, avatarEntity.Type == null)) + { + var message = Resources.Services.UserAvatarService.DatabaseCorruptedDataAndTypeNotSame; + _logger.LogCritical(message); + throw new DatabaseCorruptedException(message); + } - if (avatar.Data == null) - { - var defaultAvatar = await _defaultUserAvatarProvider.GetDefaultAvatar(); - defaultAvatar.LastModified = defaultAvatar.LastModified > avatar.LastModified ? defaultAvatar.LastModified : avatar.LastModified; - return defaultAvatar; - } - else - { - return new AvatarInfo + if (avatarEntity.Data != null) { - Avatar = new Avatar + return new AvatarInfo { - Type = avatar.Type, - Data = avatar.Data - }, - LastModified = avatar.LastModified - }; + Avatar = new Avatar + { + Type = avatarEntity.Type!, + Data = avatarEntity.Data + }, + LastModified = avatarEntity.LastModified + }; + } } + var defaultAvatar = await _defaultUserAvatarProvider.GetDefaultAvatar(); + if (avatarEntity != null) + defaultAvatar.LastModified = defaultAvatar.LastModified > avatarEntity.LastModified ? defaultAvatar.LastModified : avatarEntity.LastModified; + return defaultAvatar; } - public async Task SetAvatar(string username, Avatar avatar) + public async Task SetAvatar(string username, Avatar? avatar) { 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)); + throw new ArgumentException(Resources.Services.UserAvatarService.ArgumentAvatarDataNull, nameof(avatar)); + if (avatar.Type == null) + throw new ArgumentException(Resources.Services.UserAvatarService.ArgumentAvatarTypeNull, nameof(avatar)); } - var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username); - - var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == userId).SingleAsync(); + var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, _usernameValidator, username); + var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == userId).SingleOrDefaultAsync(); if (avatar == null) { - if (avatarEntity.Data == null) + if (avatarEntity == null || avatarEntity.Data == null) + { return; + } else { avatarEntity.Data = null; @@ -285,18 +265,29 @@ namespace Timeline.Services avatarEntity.ETag = null; avatarEntity.LastModified = DateTime.Now; await _database.SaveChangesAsync(); - _logger.LogInformation("Updated an entry in user_avatars."); + _logger.LogInformation(Resources.Services.UserAvatarService.LogUpdateEntity); } } else { await _avatarValidator.Validate(avatar); - avatarEntity.Type = avatar.Type; + var create = avatarEntity == null; + if (create) + { + avatarEntity = new UserAvatar(); + } + avatarEntity!.Type = avatar.Type; avatarEntity.Data = avatar.Data; avatarEntity.ETag = _eTagGenerator.Generate(avatar.Data); avatarEntity.LastModified = DateTime.Now; + if (create) + { + _database.UserAvatars.Add(avatarEntity); + } await _database.SaveChangesAsync(); - _logger.LogInformation("Updated an entry in user_avatars."); + _logger.LogInformation(create ? + Resources.Services.UserAvatarService.LogCreateEntity + : Resources.Services.UserAvatarService.LogUpdateEntity); } } } @@ -308,7 +299,7 @@ namespace Timeline.Services services.TryAddTransient(); services.AddScoped(); services.AddSingleton(); - services.AddSingleton(); + services.AddTransient(); } } } -- cgit v1.2.3