From 253b06dfaa091d986a8714c081fd1e01679f538a Mon Sep 17 00:00:00 2001 From: crupest Date: Wed, 10 Feb 2021 02:03:06 +0800 Subject: ... --- BackEnd/Timeline/Helpers/Cache/CacheableDataDigest.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 BackEnd/Timeline/Helpers/Cache/CacheableDataDigest.cs (limited to 'BackEnd/Timeline/Helpers/Cache/CacheableDataDigest.cs') diff --git a/BackEnd/Timeline/Helpers/Cache/CacheableDataDigest.cs b/BackEnd/Timeline/Helpers/Cache/CacheableDataDigest.cs new file mode 100644 index 00000000..3b5bcf52 --- /dev/null +++ b/BackEnd/Timeline/Helpers/Cache/CacheableDataDigest.cs @@ -0,0 +1,16 @@ +using System; + +namespace Timeline.Helpers.Cache +{ + public class CacheableDataDigest + { + public CacheableDataDigest(string eTag, DateTime lastModified) + { + ETag = eTag; + LastModified = lastModified; + } + + public string ETag { get; set; } + public DateTime LastModified { get; set; } + } +} -- cgit v1.2.3 From 02beb8b7fa3b8e5fe33307dd63de92d1c9bfaf0a Mon Sep 17 00:00:00 2001 From: crupest Date: Wed, 10 Feb 2021 19:17:08 +0800 Subject: ... --- .../Timeline/Helpers/Cache/CacheableDataDigest.cs | 2 +- BackEnd/Timeline/Models/ByteData.cs | 8 +- BackEnd/Timeline/Services/BasicUserService.cs | 29 +++ BackEnd/Timeline/Services/DataManager.cs | 27 ++- BackEnd/Timeline/Services/UserAvatarService.cs | 245 +++++++++++---------- 5 files changed, 189 insertions(+), 122 deletions(-) (limited to 'BackEnd/Timeline/Helpers/Cache/CacheableDataDigest.cs') diff --git a/BackEnd/Timeline/Helpers/Cache/CacheableDataDigest.cs b/BackEnd/Timeline/Helpers/Cache/CacheableDataDigest.cs index 3b5bcf52..18a6c894 100644 --- a/BackEnd/Timeline/Helpers/Cache/CacheableDataDigest.cs +++ b/BackEnd/Timeline/Helpers/Cache/CacheableDataDigest.cs @@ -2,7 +2,7 @@ using System; namespace Timeline.Helpers.Cache { - public class CacheableDataDigest + public class CacheableDataDigest : ICacheableDataDigest { public CacheableDataDigest(string eTag, DateTime lastModified) { diff --git a/BackEnd/Timeline/Models/ByteData.cs b/BackEnd/Timeline/Models/ByteData.cs index 7b832eb5..a1a0c238 100644 --- a/BackEnd/Timeline/Models/ByteData.cs +++ b/BackEnd/Timeline/Models/ByteData.cs @@ -1,4 +1,5 @@ -using NSwag.Annotations; +using System; +using NSwag.Annotations; namespace Timeline.Models { @@ -14,6 +15,11 @@ namespace Timeline.Models /// The content type. public ByteData(byte[] data, string contentType) { + if (data is null) + throw new ArgumentNullException(nameof(data)); + if (contentType is null) + throw new ArgumentNullException(nameof(contentType)); + Data = data; ContentType = contentType; } diff --git a/BackEnd/Timeline/Services/BasicUserService.cs b/BackEnd/Timeline/Services/BasicUserService.cs index fbbb6677..de0829ee 100644 --- a/BackEnd/Timeline/Services/BasicUserService.cs +++ b/BackEnd/Timeline/Services/BasicUserService.cs @@ -29,6 +29,14 @@ namespace Timeline.Services /// Thrown when is of bad format. /// Thrown when the user with given username does not exist. Task GetUserIdByUsername(string username); + + /// + /// Get the username modified time of a user. + /// + /// User id. + /// The time. + /// Thrown when user does not exist. + Task GetUsernameLastModifiedTime(long userId); } public class BasicUserService : IBasicUserService @@ -62,5 +70,26 @@ namespace Timeline.Services return entity.Id; } + + public async Task GetUsernameLastModifiedTime(long userId) + { + var entity = await _database.Users.Where(u => u.Id == userId).Select(u => new { u.UsernameChangeTime }).SingleOrDefaultAsync(); + + if (entity is null) + throw new UserNotExistException(userId); + + return entity.UsernameChangeTime; + } + } + + public static class BasicUserServiceExtensions + { + public static async Task ThrowIfUserNotExist(this IBasicUserService service, long userId) + { + if (!await service.CheckUserExistence(userId)) + { + throw new UserNotExistException(userId); + } + } } } diff --git a/BackEnd/Timeline/Services/DataManager.cs b/BackEnd/Timeline/Services/DataManager.cs index d447b0d5..f24bb59b 100644 --- a/BackEnd/Timeline/Services/DataManager.cs +++ b/BackEnd/Timeline/Services/DataManager.cs @@ -38,13 +38,12 @@ namespace Timeline.Services public Task FreeEntry(string tag); /// - /// Retrieve the entry with given tag. + /// Retrieve the entry with given tag. If not exist, returns null. /// /// The tag of the entry. - /// The data of the entry. + /// The data of the entry. If not exist, returns null. /// Thrown when is null. - /// Thrown when entry with given tag does not exist. - public Task GetEntry(string tag); + public Task GetEntry(string tag); } public class DataManager : IDataManager @@ -106,17 +105,31 @@ namespace Timeline.Services } } - public async Task GetEntry(string tag) + public async Task GetEntry(string tag) { if (tag == null) throw new ArgumentNullException(nameof(tag)); var entity = await _database.Data.Where(d => d.Tag == tag).Select(d => new { d.Data }).SingleOrDefaultAsync(); - if (entity == null) - throw new InvalidOperationException(Resources.Services.DataManager.ExceptionEntryNotExist); + if (entity is null) + return null; return entity.Data; } } + + public static class DataManagerExtensions + { + /// + /// Try to get an entry and throw if not exist. + /// + public static async Task GetEntryAndCheck(this IDataManager dataManager, string tag, string notExistMessage) + { + var data = await dataManager.GetEntry(tag); + if (data is null) + throw new DatabaseCorruptedException($"Can't get data of tag {tag}. {notExistMessage}"); + return data; + } + } } 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