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.GenerateETagAsync(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();
}
}
}