From 02beb8b7fa3b8e5fe33307dd63de92d1c9bfaf0a Mon Sep 17 00:00:00 2001 From: crupest Date: Wed, 10 Feb 2021 19:17:08 +0800 Subject: ... --- BackEnd/Timeline/Services/UserAvatarService.cs | 245 +++++++++++++------------ 1 file changed, 132 insertions(+), 113 deletions(-) (limited to 'BackEnd/Timeline/Services/UserAvatarService.cs') diff --git a/BackEnd/Timeline/Services/UserAvatarService.cs b/BackEnd/Timeline/Services/UserAvatarService.cs index afd6cf0a..5a6d013e 100644 --- a/BackEnd/Timeline/Services/UserAvatarService.cs +++ b/BackEnd/Timeline/Services/UserAvatarService.cs @@ -2,12 +2,12 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using SixLabors.ImageSharp; using System; using System.IO; using System.Linq; using System.Threading.Tasks; using Timeline.Entities; -using Timeline.Helpers; using Timeline.Helpers.Cache; using Timeline.Models; using Timeline.Services.Exceptions; @@ -23,39 +23,53 @@ namespace Timeline.Services public interface IDefaultUserAvatarProvider { /// - /// Get the etag of default avatar. + /// Get the digest of default avatar. /// - /// - Task GetDefaultAvatarETag(); + /// The digest. + Task GetDefaultAvatarDigest(); /// /// Get the default avatar. /// + /// The avatar. Task GetDefaultAvatar(); } public interface IUserAvatarService { - Task GetAvatarDigest(long id); + /// + /// Get avatar digest of a user. + /// + /// User id. + /// The avatar digest. + /// Thrown when user does not exist. + Task GetAvatarDigest(long userId); /// - /// Get avatar of a user. If the user has no avatar set, a default one is returned. Warning: This method does not check the user existence. + /// Get avatar of a user. If the user has no avatar set, a default one is returned. /// - /// The id of the user to get avatar of. - /// The avatar info. - Task GetAvatar(long id); + /// User id. + /// The avatar. + /// Thrown when user does not exist. + Task GetAvatar(long userId); /// - /// Set avatar for a user. Warning: This method does not check the user existence. + /// Set avatar for a user. /// - /// The id of the user to set avatar for. - /// The avatar. Can be null to delete the saved avatar. - /// The etag of the avatar. - /// Thrown if any field in is null when is not null. + /// User id. + /// The new avatar data. + /// The digest of the avatar. + /// Thrown if is null. + /// Thrown when user does not exist. /// Thrown if avatar is of bad format. - Task SetAvatar(long id, ByteData avatar); + Task SetAvatar(long userId, ByteData avatar); - Task DeleteAvatar(long id); + /// + /// Remove avatar of a user. + /// + /// User id. + /// Thrown when user does not exist. + Task DeleteAvatar(long userId); } // TODO! : Make this configurable. @@ -65,9 +79,8 @@ namespace Timeline.Services private readonly string _avatarPath; - private byte[] _cacheData = default!; - private DateTime _cacheLastModified; - private string _cacheETag = default!; + private CacheableDataDigest? _cacheDigest; + private ByteData? _cacheData; public DefaultUserAvatarProvider(IWebHostEnvironment environment, IETagGenerator eTagGenerator) { @@ -78,53 +91,42 @@ namespace Timeline.Services private async Task CheckAndInit() { var path = _avatarPath; - if (_cacheData == null || File.GetLastWriteTime(path) > _cacheLastModified) + if (_cacheData == null || File.GetLastWriteTime(path) > _cacheDigest!.LastModified) { - _cacheData = await File.ReadAllBytesAsync(path); - _cacheLastModified = File.GetLastWriteTime(path); - _cacheETag = await _eTagGenerator.Generate(_cacheData); + var data = await File.ReadAllBytesAsync(path); + _cacheDigest = new CacheableDataDigest(await _eTagGenerator.Generate(data), File.GetLastWriteTime(path)); + Image.Identify(data, out var format); + _cacheData = new ByteData(data, format.DefaultMimeType); } } - public async Task GetDefaultAvatarETag() + public async Task GetDefaultAvatarDigest() { await CheckAndInit(); - return _cacheETag; + return _cacheDigest!; } - public async Task GetDefaultAvatar() + public async Task GetDefaultAvatar() { await CheckAndInit(); - return new AvatarInfo - { - Avatar = new Avatar - { - Type = "image/png", - Data = _cacheData - }, - LastModified = _cacheLastModified - }; + return _cacheData!; } } public class UserAvatarService : IUserAvatarService { - private readonly ILogger _logger; - private readonly DatabaseContext _database; - + private readonly IBasicUserService _basicUserService; private readonly IDefaultUserAvatarProvider _defaultUserAvatarProvider; - private readonly IImageValidator _imageValidator; - private readonly IDataManager _dataManager; - private readonly IClock _clock; public UserAvatarService( ILogger logger, DatabaseContext database, + IBasicUserService basicUserService, IDefaultUserAvatarProvider defaultUserAvatarProvider, IImageValidator imageValidator, IDataManager dataManager, @@ -132,106 +134,123 @@ namespace Timeline.Services { _logger = logger; _database = database; + _basicUserService = basicUserService; _defaultUserAvatarProvider = defaultUserAvatarProvider; _imageValidator = imageValidator; _dataManager = dataManager; _clock = clock; } - public async Task GetAvatarETag(long id) + public async Task GetAvatarDigest(long userId) { - var eTag = (await _database.UserAvatars.Where(a => a.UserId == id).Select(a => new { a.DataTag }).SingleOrDefaultAsync())?.DataTag; - if (eTag == null) - return await _defaultUserAvatarProvider.GetDefaultAvatarETag(); + var usernameChangeTime = await _basicUserService.GetUsernameLastModifiedTime(userId); + + var entity = await _database.UserAvatars.Where(a => a.UserId == userId).Select(a => new { a.DataTag, a.LastModified }).SingleOrDefaultAsync(); + + if (entity is null) + { + var defaultAvatarDigest = await _defaultUserAvatarProvider.GetDefaultAvatarDigest(); + return new CacheableDataDigest(defaultAvatarDigest.ETag, new DateTime[] { usernameChangeTime, defaultAvatarDigest.LastModified }.Max()); + } + else if (entity.DataTag is null) + { + var defaultAvatarDigest = await _defaultUserAvatarProvider.GetDefaultAvatarDigest(); + return new CacheableDataDigest(defaultAvatarDigest.ETag, new DateTime[] { usernameChangeTime, defaultAvatarDigest.LastModified, entity.LastModified }.Max()); + } else - return eTag; + { + return new CacheableDataDigest(entity.DataTag, new DateTime[] { usernameChangeTime, entity.LastModified }.Max()); + } } - public async Task GetAvatar(long id) + public async Task GetAvatar(long userId) { - var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == id).Select(a => new { a.Type, a.DataTag, a.LastModified }).SingleOrDefaultAsync(); + await _basicUserService.ThrowIfUserNotExist(userId); + + var entity = await _database.UserAvatars.Where(a => a.UserId == userId).SingleOrDefaultAsync(); - if (avatarEntity != null) + if (entity is null || entity.DataTag is null) { - if (!LanguageHelper.AreSame(avatarEntity.DataTag == null, avatarEntity.Type == null)) - { - var message = Resources.Services.UserAvatarService.ExceptionDatabaseCorruptedDataAndTypeNotSame; - _logger.LogCritical(message); - throw new DatabaseCorruptedException(message); - } + return await _defaultUserAvatarProvider.GetDefaultAvatar(); + } + var data = await _dataManager.GetEntryAndCheck(entity.DataTag, $"This is required by avatar of {userId}."); - if (avatarEntity.DataTag != null) - { - var data = await _dataManager.GetEntry(avatarEntity.DataTag); - return new AvatarInfo - { - Avatar = new Avatar - { - Type = avatarEntity.Type!, - Data = data - }, - LastModified = avatarEntity.LastModified - }; - } + if (entity.Type is null) + { + Image.Identify(data, out var format); + entity.Type = format.DefaultMimeType; + await _database.SaveChangesAsync(); } - var defaultAvatar = await _defaultUserAvatarProvider.GetDefaultAvatar(); - if (avatarEntity != null) - defaultAvatar.LastModified = defaultAvatar.LastModified > avatarEntity.LastModified ? defaultAvatar.LastModified : avatarEntity.LastModified; - return defaultAvatar; + + return new ByteData(data, entity.Type); } - public async Task SetAvatar(long id, Avatar? avatar) + public async Task SetAvatar(long userId, ByteData avatar) { - if (avatar != null) - { - if (avatar.Data == null) - throw new ArgumentException(Resources.Services.UserAvatarService.ExceptionAvatarDataNull, nameof(avatar)); - if (string.IsNullOrEmpty(avatar.Type)) - throw new ArgumentException(Resources.Services.UserAvatarService.ExceptionAvatarTypeNullOrEmpty, nameof(avatar)); - } + if (avatar is null) + throw new ArgumentNullException(nameof(avatar)); + + await _imageValidator.Validate(avatar.Data, avatar.ContentType, true); + + await _basicUserService.ThrowIfUserNotExist(userId); + + var entity = await _database.UserAvatars.Where(a => a.UserId == userId).SingleOrDefaultAsync(); - var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == id).SingleOrDefaultAsync(); + await using var transaction = await _database.Database.BeginTransactionAsync(); - if (avatar == null) + var tag = await _dataManager.RetainEntry(avatar.Data); + + var now = _clock.GetCurrentTime(); + + if (entity is null) { - if (avatarEntity != null && avatarEntity.DataTag != null) + var newEntity = new UserAvatarEntity { - await _dataManager.FreeEntry(avatarEntity.DataTag); - avatarEntity.DataTag = null; - avatarEntity.Type = null; - avatarEntity.LastModified = _clock.GetCurrentTime(); - await _database.SaveChangesAsync(); - _logger.LogInformation(Resources.Services.UserAvatarService.LogUpdateEntity); - } - return await _defaultUserAvatarProvider.GetDefaultAvatarETag(); + DataTag = tag, + Type = avatar.ContentType, + LastModified = now, + UserId = userId + }; + _database.Add(newEntity); } else { - await _imageValidator.Validate(avatar.Data, avatar.Type, true); - var tag = await _dataManager.RetainEntry(avatar.Data); - var oldTag = avatarEntity?.DataTag; - var create = avatarEntity == null; - if (avatarEntity == null) - { - avatarEntity = new UserAvatarEntity(); - _database.UserAvatars.Add(avatarEntity); - } - avatarEntity.DataTag = tag; - avatarEntity.Type = avatar.Type; - avatarEntity.LastModified = _clock.GetCurrentTime(); - avatarEntity.UserId = id; - await _database.SaveChangesAsync(); - _logger.LogInformation(create ? - Resources.Services.UserAvatarService.LogCreateEntity - : Resources.Services.UserAvatarService.LogUpdateEntity); - if (oldTag != null) - { - await _dataManager.FreeEntry(oldTag); - } + if (entity.DataTag is not null) + await _dataManager.FreeEntry(entity.DataTag); - return avatarEntity.DataTag; + entity.DataTag = tag; + entity.Type = avatar.ContentType; + entity.LastModified = now; } + + await _database.SaveChangesAsync(); + + await transaction.CommitAsync(); + + return new CacheableDataDigest(tag, now); + } + + public async Task DeleteAvatar(long userId) + { + await _basicUserService.ThrowIfUserNotExist(userId); + + var entity = await _database.UserAvatars.Where(a => a.UserId == userId).SingleOrDefaultAsync(); + + if (entity is null || entity.DataTag is null) + return; + + await using var transaction = await _database.Database.BeginTransactionAsync(); + + await _dataManager.FreeEntry(entity.DataTag); + + entity.DataTag = null; + entity.Type = null; + entity.LastModified = _clock.GetCurrentTime(); + + await _database.SaveChangesAsync(); + + await transaction.CommitAsync(); } } -- cgit v1.2.3