From ac769e656b122ff569c3f1534701b71e00fed586 Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 27 Oct 2020 19:21:35 +0800 Subject: Split front and back end. --- BackEnd/Timeline/Services/UserAvatarService.cs | 265 +++++++++++++++++++++++++ 1 file changed, 265 insertions(+) create mode 100644 BackEnd/Timeline/Services/UserAvatarService.cs (limited to 'BackEnd/Timeline/Services/UserAvatarService.cs') diff --git a/BackEnd/Timeline/Services/UserAvatarService.cs b/BackEnd/Timeline/Services/UserAvatarService.cs new file mode 100644 index 00000000..b41c45fd --- /dev/null +++ b/BackEnd/Timeline/Services/UserAvatarService.cs @@ -0,0 +1,265 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Timeline.Entities; +using Timeline.Helpers; +using Timeline.Services.Exceptions; + +namespace Timeline.Services +{ + public class Avatar + { + 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; } = default!; + public DateTime LastModified { get; set; } + + public CacheableData ToCacheableData() + { + return new CacheableData(Avatar.Type, Avatar.Data, LastModified); + } + } + + /// + /// Provider for default user avatar. + /// + /// + /// Mainly for unit tests. + /// + public interface IDefaultUserAvatarProvider + { + /// + /// Get the etag of default avatar. + /// + /// + Task GetDefaultAvatarETag(); + + /// + /// Get the default avatar. + /// + Task GetDefaultAvatar(); + } + + public interface IUserAvatarService + { + /// + /// Get the etag of a user's avatar. Warning: This method does not check the user existence. + /// + /// The id of the user to get avatar etag of. + /// The etag. + Task GetAvatarETag(long id); + + /// + /// 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. + /// + /// The id of the user to get avatar of. + /// The avatar info. + Task GetAvatar(long id); + + /// + /// Set avatar for a user. Warning: This method does not check the user existence. + /// + /// 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. + /// Thrown if avatar is of bad format. + Task SetAvatar(long id, Avatar? avatar); + } + + // TODO! : Make this configurable. + public class DefaultUserAvatarProvider : IDefaultUserAvatarProvider + { + private readonly IETagGenerator _eTagGenerator; + + private readonly string _avatarPath; + + private byte[] _cacheData = default!; + private DateTime _cacheLastModified; + private string _cacheETag = default!; + + 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) > _cacheLastModified) + { + _cacheData = await File.ReadAllBytesAsync(path); + _cacheLastModified = File.GetLastWriteTime(path); + _cacheETag = await _eTagGenerator.Generate(_cacheData); + } + } + + public async Task GetDefaultAvatarETag() + { + await CheckAndInit(); + return _cacheETag; + } + + public async Task GetDefaultAvatar() + { + await CheckAndInit(); + return new AvatarInfo + { + Avatar = new Avatar + { + Type = "image/png", + Data = _cacheData + }, + LastModified = _cacheLastModified + }; + } + } + + public class UserAvatarService : IUserAvatarService + { + + private readonly ILogger _logger; + + private readonly DatabaseContext _database; + + private readonly IDefaultUserAvatarProvider _defaultUserAvatarProvider; + + private readonly IImageValidator _imageValidator; + + private readonly IDataManager _dataManager; + + private readonly IClock _clock; + + public UserAvatarService( + ILogger logger, + DatabaseContext database, + IDefaultUserAvatarProvider defaultUserAvatarProvider, + IImageValidator imageValidator, + IDataManager dataManager, + IClock clock) + { + _logger = logger; + _database = database; + _defaultUserAvatarProvider = defaultUserAvatarProvider; + _imageValidator = imageValidator; + _dataManager = dataManager; + _clock = clock; + } + + public async Task GetAvatarETag(long id) + { + var eTag = (await _database.UserAvatars.Where(a => a.UserId == id).Select(a => new { a.DataTag }).SingleOrDefaultAsync())?.DataTag; + if (eTag == null) + return await _defaultUserAvatarProvider.GetDefaultAvatarETag(); + else + return eTag; + } + + public async Task GetAvatar(long id) + { + var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == id).Select(a => new { a.Type, a.DataTag, a.LastModified }).SingleOrDefaultAsync(); + + if (avatarEntity != null) + { + if (!LanguageHelper.AreSame(avatarEntity.DataTag == null, avatarEntity.Type == null)) + { + var message = Resources.Services.UserAvatarService.ExceptionDatabaseCorruptedDataAndTypeNotSame; + _logger.LogCritical(message); + throw new DatabaseCorruptedException(message); + } + + + 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 + }; + } + } + var defaultAvatar = await _defaultUserAvatarProvider.GetDefaultAvatar(); + if (avatarEntity != null) + defaultAvatar.LastModified = defaultAvatar.LastModified > avatarEntity.LastModified ? defaultAvatar.LastModified : avatarEntity.LastModified; + return defaultAvatar; + } + + public async Task SetAvatar(long id, Avatar? 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)); + } + + var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == id).SingleOrDefaultAsync(); + + if (avatar == null) + { + if (avatarEntity != null && avatarEntity.DataTag != null) + { + 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(); + } + 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); + } + + return avatarEntity.DataTag; + } + } + } + + public static class UserAvatarServiceCollectionExtensions + { + public static void AddUserAvatarService(this IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + } + } +} -- cgit v1.2.3