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; 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. /// 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) { return; } else { await _dataManager.FreeEntry(avatarEntity.DataTag); avatarEntity.DataTag = null; avatarEntity.Type = null; avatarEntity.LastModified = _clock.GetCurrentTime(); await _database.SaveChangesAsync(); _logger.LogInformation(Resources.Services.UserAvatarService.LogUpdateEntity); } } 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); } } } } public static class UserAvatarServiceCollectionExtensions { public static void AddUserAvatarService(this IServiceCollection services) { services.AddScoped(); services.AddScoped(); } } }