using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats;
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Timeline.Entities;
namespace Timeline.Services
{
public class Avatar
{
public string Type { get; set; }
public byte[] Data { get; set; }
}
public class AvatarInfo
{
public Avatar Avatar { get; set; }
public DateTime LastModified { get; set; }
}
///
/// Thrown when avatar is of bad format.
///
[Serializable]
public class AvatarDataException : Exception
{
public enum ErrorReason
{
///
/// Decoding image failed.
///
CantDecode,
///
/// Decoding succeeded but the real type is not the specified type.
///
UnmatchedFormat,
///
/// Image is not a square.
///
BadSize
}
public AvatarDataException(Avatar avatar, ErrorReason error, string message) : base(message) { Avatar = avatar; Error = error; }
public AvatarDataException(Avatar avatar, ErrorReason error, string message, Exception inner) : base(message, inner) { Avatar = avatar; Error = error; }
protected AvatarDataException(
System.Runtime.Serialization.SerializationInfo info,
System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
public ErrorReason Error { get; set; }
public Avatar Avatar { get; set; }
}
///
/// 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 IUserAvatarValidator
{
///
/// Validate a avatar's format and size info.
///
/// The avatar to validate.
/// Thrown when validation failed.
Task Validate(Avatar avatar);
}
public interface IUserAvatarService
{
///
/// Get the etag of a user's avatar.
///
/// The username of the user to get avatar etag of.
/// The etag.
/// Thrown if is null or empty.
/// Thrown if the user does not exist.
Task GetAvatarETag(string username);
///
/// Get avatar of a user. If the user has no avatar, a default one is returned.
///
/// The username of the user to get avatar of.
/// The avatar info.
/// Thrown if is null or empty.
/// Thrown if the user does not exist.
Task GetAvatar(string username);
///
/// Set avatar for a user.
///
/// The username of the user to set avatar for.
/// The avatar. Can be null to delete the saved avatar.
/// Throw if is null or empty.
/// Or thrown if is not null but is null or empty or is null.
/// Thrown if the user does not exist.
/// Thrown if avatar is of bad format.
Task SetAvatar(string username, Avatar avatar);
}
public class DefaultUserAvatarProvider : IDefaultUserAvatarProvider
{
private readonly IWebHostEnvironment _environment;
private readonly IETagGenerator _eTagGenerator;
private byte[] _cacheData;
private DateTime _cacheLastModified;
private string _cacheETag;
public DefaultUserAvatarProvider(IWebHostEnvironment environment, IETagGenerator eTagGenerator)
{
_environment = environment;
_eTagGenerator = eTagGenerator;
}
private async Task CheckAndInit()
{
if (_cacheData != null)
return;
var path = Path.Combine(_environment.ContentRootPath, "default-avatar.png");
_cacheData = await File.ReadAllBytesAsync(path);
_cacheLastModified = File.GetLastWriteTime(path);
_cacheETag = _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 UserAvatarValidator : IUserAvatarValidator
{
public Task Validate(Avatar avatar)
{
return Task.Run(() =>
{
try
{
using (var image = Image.Load(avatar.Data, out IImageFormat format))
{
if (!format.MimeTypes.Contains(avatar.Type))
throw new AvatarDataException(avatar, AvatarDataException.ErrorReason.UnmatchedFormat, "Image's actual mime type is not the specified one.");
if (image.Width != image.Height)
throw new AvatarDataException(avatar, AvatarDataException.ErrorReason.BadSize, "Image is not a square, aka, width is not equal to height.");
}
}
catch (UnknownImageFormatException e)
{
throw new AvatarDataException(avatar, AvatarDataException.ErrorReason.CantDecode, "Failed to decode image. See inner exception.", e);
}
});
}
}
public class UserAvatarService : IUserAvatarService
{
private readonly ILogger _logger;
private readonly DatabaseContext _database;
private readonly IDefaultUserAvatarProvider _defaultUserAvatarProvider;
private readonly IUserAvatarValidator _avatarValidator;
private readonly IETagGenerator _eTagGenerator;
public UserAvatarService(
ILogger logger,
DatabaseContext database,
IDefaultUserAvatarProvider defaultUserAvatarProvider,
IUserAvatarValidator avatarValidator,
IETagGenerator eTagGenerator)
{
_logger = logger;
_database = database;
_defaultUserAvatarProvider = defaultUserAvatarProvider;
_avatarValidator = avatarValidator;
_eTagGenerator = eTagGenerator;
}
public async Task GetAvatarETag(string username)
{
var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username);
var eTag = (await _database.UserAvatars.Where(a => a.UserId == userId).Select(a => new { a.ETag }).SingleAsync()).ETag;
if (eTag == null)
return await _defaultUserAvatarProvider.GetDefaultAvatarETag();
else
return eTag;
}
public async Task GetAvatar(string username)
{
var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username);
var avatar = await _database.UserAvatars.Where(a => a.UserId == userId).Select(a => new { a.Type, a.Data, a.LastModified }).SingleAsync();
if ((avatar.Type == null) != (avatar.Data == null))
{
_logger.LogCritical("Database corupted! One of type and data of a avatar is null but the other is not.");
throw new DatabaseCorruptedException();
}
if (avatar.Data == null)
{
var defaultAvatar = await _defaultUserAvatarProvider.GetDefaultAvatar();
defaultAvatar.LastModified = defaultAvatar.LastModified > avatar.LastModified ? defaultAvatar.LastModified : avatar.LastModified;
return defaultAvatar;
}
else
{
return new AvatarInfo
{
Avatar = new Avatar
{
Type = avatar.Type,
Data = avatar.Data
},
LastModified = avatar.LastModified
};
}
}
public async Task SetAvatar(string username, Avatar avatar)
{
if (avatar != null)
{
if (string.IsNullOrEmpty(avatar.Type))
throw new ArgumentException("Type of avatar is null or empty.", nameof(avatar));
if (avatar.Data == null)
throw new ArgumentException("Data of avatar is null.", nameof(avatar));
}
var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username);
var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == userId).SingleAsync();
if (avatar == null)
{
if (avatarEntity.Data == null)
return;
else
{
avatarEntity.Data = null;
avatarEntity.Type = null;
avatarEntity.ETag = null;
avatarEntity.LastModified = DateTime.Now;
await _database.SaveChangesAsync();
_logger.LogInformation("Updated an entry in user_avatars.");
}
}
else
{
await _avatarValidator.Validate(avatar);
avatarEntity.Type = avatar.Type;
avatarEntity.Data = avatar.Data;
avatarEntity.ETag = _eTagGenerator.Generate(avatar.Data);
avatarEntity.LastModified = DateTime.Now;
await _database.SaveChangesAsync();
_logger.LogInformation("Updated an entry in user_avatars.");
}
}
}
public static class UserAvatarServiceCollectionExtensions
{
public static void AddUserAvatarService(this IServiceCollection services)
{
services.TryAddTransient();
services.AddScoped();
services.AddSingleton();
services.AddSingleton();
}
}
}