From 657fb589137099794e58fbd35beb7d942b376965 Mon Sep 17 00:00:00 2001 From: crupest Date: Sun, 25 Apr 2021 21:20:04 +0800 Subject: ... --- .../Timeline/Services/User/UserAvatarService.cs | 266 +++++++++++++++++++++ 1 file changed, 266 insertions(+) create mode 100644 BackEnd/Timeline/Services/User/UserAvatarService.cs (limited to 'BackEnd/Timeline/Services/User/UserAvatarService.cs') diff --git a/BackEnd/Timeline/Services/User/UserAvatarService.cs b/BackEnd/Timeline/Services/User/UserAvatarService.cs new file mode 100644 index 00000000..0a4b7438 --- /dev/null +++ b/BackEnd/Timeline/Services/User/UserAvatarService.cs @@ -0,0 +1,266 @@ +using Microsoft.AspNetCore.Hosting; +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.Cache; +using Timeline.Models; +using Timeline.Services.Data; +using Timeline.Services.Imaging; + +namespace Timeline.Services.User +{ + /// + /// Provider for default user avatar. + /// + /// + /// Mainly for unit tests. + /// + public interface IDefaultUserAvatarProvider + { + /// + /// Get the digest of default avatar. + /// + /// The digest. + Task GetDefaultAvatarDigest(); + + /// + /// Get the default avatar. + /// + /// The avatar. + Task GetDefaultAvatar(); + } + + public interface IUserAvatarService + { + /// + /// 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. + /// + /// User id. + /// The avatar. + /// Thrown when user does not exist. + Task GetAvatar(long userId); + + /// + /// Set avatar for a user. + /// + /// 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 userId, ByteData avatar); + + /// + /// Remove avatar of a user. + /// + /// User id. + /// Thrown when user does not exist. + Task DeleteAvatar(long userId); + } + + // TODO! : Make this configurable. + public class DefaultUserAvatarProvider : IDefaultUserAvatarProvider + { + private readonly IETagGenerator _eTagGenerator; + + private readonly string _avatarPath; + + private CacheableDataDigest? _cacheDigest; + private ByteData? _cacheData; + + public DefaultUserAvatarProvider(IWebHostEnvironment environment, IETagGenerator eTagGenerator) + { + _avatarPath = Path.Combine(environment.ContentRootPath, "default-avatar.png"); + _eTagGenerator = eTagGenerator; + } + + private async Task CheckAndInit() + { + var path = _avatarPath; + if (_cacheData == null || File.GetLastWriteTime(path) > _cacheDigest!.LastModified) + { + 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 GetDefaultAvatarDigest() + { + await CheckAndInit(); + return _cacheDigest!; + } + + public async Task GetDefaultAvatar() + { + await CheckAndInit(); + 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, + IClock clock) + { + _logger = logger; + _database = database; + _basicUserService = basicUserService; + _defaultUserAvatarProvider = defaultUserAvatarProvider; + _imageValidator = imageValidator; + _dataManager = dataManager; + _clock = clock; + } + + public async Task GetAvatarDigest(long userId) + { + 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 new CacheableDataDigest(entity.DataTag, new DateTime[] { usernameChangeTime, entity.LastModified }.Max()); + } + } + + public async Task GetAvatar(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 _defaultUserAvatarProvider.GetDefaultAvatar(); + } + + var data = await _dataManager.GetEntryAndCheck(entity.DataTag, $"This is required by avatar of {userId}."); + + if (entity.Type is null) + { + Image.Identify(data, out var format); + entity.Type = format.DefaultMimeType; + await _database.SaveChangesAsync(); + } + + return new ByteData(data, entity.Type); + } + + public async Task SetAvatar(long userId, ByteData 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(); + + await using var transaction = await _database.Database.BeginTransactionAsync(); + + var tag = await _dataManager.RetainEntry(avatar.Data); + + var now = _clock.GetCurrentTime(); + + if (entity is null) + { + var newEntity = new UserAvatarEntity + { + DataTag = tag, + Type = avatar.ContentType, + LastModified = now, + UserId = userId + }; + _database.Add(newEntity); + } + else + { + if (entity.DataTag is not null) + await _dataManager.FreeEntry(entity.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(); + } + } + + public static class UserAvatarServiceCollectionExtensions + { + public static void AddUserAvatarService(this IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + } + } +} -- cgit v1.2.3