diff options
author | 杨宇千 <crupest@outlook.com> | 2019-08-19 16:12:16 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-08-19 16:12:16 +0800 |
commit | 134173eda92de04961dc69757b257c1c547d88a4 (patch) | |
tree | bd8c0d70aac7cd4d1a6c28ae7c9b7c681dbd5613 | |
parent | 24fe6340ea69321ecafb57c8c5d6cd4b72f229b4 (diff) | |
parent | 79e578e97ed252bff0dca3c89d81a395b35289d7 (diff) | |
download | timeline-134173eda92de04961dc69757b257c1c547d88a4.tar.gz timeline-134173eda92de04961dc69757b257c1c547d88a4.tar.bz2 timeline-134173eda92de04961dc69757b257c1c547d88a4.zip |
Merge pull request #45 from crupest/avatar-cache
Add 304 response for If-Modified-Since in avatar.
-rw-r--r-- | Timeline.Tests/DatabaseTest.cs | 37 | ||||
-rw-r--r-- | Timeline.Tests/Helpers/MyWebApplicationFactory.cs | 4 | ||||
-rw-r--r-- | Timeline.Tests/IntegratedTests/UserAvatarTests.cs | 12 | ||||
-rw-r--r-- | Timeline.Tests/Mock/Data/TestDatabase.cs | 12 | ||||
-rw-r--r-- | Timeline.Tests/Mock/Data/TestUsers.cs | 40 | ||||
-rw-r--r-- | Timeline.Tests/UserAvatarServiceTest.cs | 17 | ||||
-rw-r--r-- | Timeline/Controllers/UserAvatarController.cs | 16 | ||||
-rw-r--r-- | Timeline/Entities/DatabaseContext.cs | 3 | ||||
-rw-r--r-- | Timeline/Entities/UserAvatar.cs | 23 | ||||
-rw-r--r-- | Timeline/Migrations/20190819074906_AddAvatarLastModified.Designer.cs | 86 | ||||
-rw-r--r-- | Timeline/Migrations/20190819074906_AddAvatarLastModified.cs | 114 | ||||
-rw-r--r-- | Timeline/Migrations/20190819080823_AddIndexForUserName.Designer.cs | 88 | ||||
-rw-r--r-- | Timeline/Migrations/20190819080823_AddIndexForUserName.cs | 22 | ||||
-rw-r--r-- | Timeline/Migrations/DatabaseContextModelSnapshot.cs | 23 | ||||
-rw-r--r-- | Timeline/Services/UserAvatarService.cs | 76 | ||||
-rw-r--r-- | Timeline/Services/UserService.cs | 2 |
16 files changed, 495 insertions, 80 deletions
diff --git a/Timeline.Tests/DatabaseTest.cs b/Timeline.Tests/DatabaseTest.cs new file mode 100644 index 00000000..e280637c --- /dev/null +++ b/Timeline.Tests/DatabaseTest.cs @@ -0,0 +1,37 @@ +using FluentAssertions;
+using Microsoft.EntityFrameworkCore;
+using System;
+using System.Linq;
+using Timeline.Entities;
+using Timeline.Tests.Mock.Data;
+using Xunit;
+
+namespace Timeline.Tests
+{
+ public class DatabaseTest : IDisposable
+ {
+ private readonly TestDatabase _database;
+ private readonly DatabaseContext _context;
+
+ public DatabaseTest()
+ {
+ _database = new TestDatabase();
+ _context = _database.DatabaseContext;
+ }
+
+ public void Dispose()
+ {
+ _database.Dispose();
+ }
+
+ [Fact]
+ public void DeleteUserShouldAlsoDeleteAvatar()
+ {
+ _context.UserAvatars.Count().Should().Be(2);
+ var user = _context.Users.First();
+ _context.Users.Remove(user);
+ _context.SaveChanges();
+ _context.UserAvatars.Count().Should().Be(1);
+ }
+ }
+}
diff --git a/Timeline.Tests/Helpers/MyWebApplicationFactory.cs b/Timeline.Tests/Helpers/MyWebApplicationFactory.cs index e96d11fe..1a9fe01e 100644 --- a/Timeline.Tests/Helpers/MyWebApplicationFactory.cs +++ b/Timeline.Tests/Helpers/MyWebApplicationFactory.cs @@ -46,9 +46,7 @@ namespace Timeline.Tests.Helpers using (var context = new DatabaseContext(options))
{
- context.Database.EnsureCreated();
- context.Users.AddRange(MockUsers.Users);
- context.SaveChanges();
+ TestDatabase.InitDatabase(context);
};
}
diff --git a/Timeline.Tests/IntegratedTests/UserAvatarTests.cs b/Timeline.Tests/IntegratedTests/UserAvatarTests.cs index efe63346..794f251b 100644 --- a/Timeline.Tests/IntegratedTests/UserAvatarTests.cs +++ b/Timeline.Tests/IntegratedTests/UserAvatarTests.cs @@ -6,6 +6,7 @@ using SixLabors.ImageSharp.Formats.Png; using System;
using System.IO;
using System.Net;
+using System.Net.Http;
using System.Threading.Tasks;
using Timeline.Controllers;
using Timeline.Services;
@@ -64,6 +65,17 @@ namespace Timeline.Tests.IntegratedTests await GetReturnDefault("admin");
{
+ var request = new HttpRequestMessage()
+ {
+ RequestUri = new Uri(client.BaseAddress, "users/user/avatar"),
+ Method = HttpMethod.Get,
+ };
+ request.Headers.Add("If-Modified-Since", DateTime.Now.ToString("r"));
+ var res = await client.SendAsync(request);
+ res.Should().HaveStatusCode(HttpStatusCode.NotModified);
+ }
+
+ {
var res = await client.PutByteArrayAsync("users/user/avatar", new[] { (byte)0x00 }, "image/notaccept");
res.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType);
}
diff --git a/Timeline.Tests/Mock/Data/TestDatabase.cs b/Timeline.Tests/Mock/Data/TestDatabase.cs index 09c77dce..7b4bc65e 100644 --- a/Timeline.Tests/Mock/Data/TestDatabase.cs +++ b/Timeline.Tests/Mock/Data/TestDatabase.cs @@ -8,6 +8,13 @@ namespace Timeline.Tests.Mock.Data {
public class TestDatabase : IDisposable
{
+ public static void InitDatabase(DatabaseContext context)
+ {
+ context.Database.EnsureCreated();
+ context.Users.AddRange(MockUsers.CreateMockUsers());
+ context.SaveChanges();
+ }
+
private readonly SqliteConnection _databaseConnection;
private readonly DatabaseContext _databaseContext;
@@ -26,10 +33,7 @@ namespace Timeline.Tests.Mock.Data _databaseContext = new DatabaseContext(options);
- // init with mock data
- _databaseContext.Database.EnsureCreated();
- _databaseContext.Users.AddRange(MockUsers.Users);
- _databaseContext.SaveChanges();
+ InitDatabase(_databaseContext);
}
public void Dispose()
diff --git a/Timeline.Tests/Mock/Data/TestUsers.cs b/Timeline.Tests/Mock/Data/TestUsers.cs index f34f62c5..378fc280 100644 --- a/Timeline.Tests/Mock/Data/TestUsers.cs +++ b/Timeline.Tests/Mock/Data/TestUsers.cs @@ -1,3 +1,4 @@ +using System;
using System.Collections.Generic;
using System.Linq;
using Timeline.Entities;
@@ -10,38 +11,39 @@ namespace Timeline.Tests.Mock.Data {
static MockUsers()
{
- var mockUsers = new List<User>();
- var passwordService = new PasswordService();
+ var mockUserInfos = CreateMockUsers().Select(u => UserUtility.CreateUserInfo(u)).ToList();
+ UserUserInfo = mockUserInfos[0];
+ AdminUserInfo = mockUserInfos[1];
+ UserInfos = mockUserInfos;
+ }
- mockUsers.Add(new User
+ public const string UserUsername = "user";
+ public const string AdminUsername = "admin";
+ public const string UserPassword = "user";
+ public const string AdminPassword = "admin";
+
+ // emmmmmmm. Never reuse the user instances because EF Core uses them which will cause strange things.
+ internal static IEnumerable<User> CreateMockUsers()
+ {
+ var users = new List<User>();
+ var passwordService = new PasswordService();
+ users.Add(new User
{
Name = UserUsername,
EncryptedPassword = passwordService.HashPassword(UserPassword),
RoleString = UserUtility.IsAdminToRoleString(false),
- Version = 0,
+ Avatar = UserAvatar.Create(DateTime.Now)
});
- mockUsers.Add(new User
+ users.Add(new User
{
Name = AdminUsername,
EncryptedPassword = passwordService.HashPassword(AdminPassword),
RoleString = UserUtility.IsAdminToRoleString(true),
- Version = 0,
+ Avatar = UserAvatar.Create(DateTime.Now)
});
-
- Users = mockUsers;
-
- var mockUserInfos = mockUsers.Select(u => UserUtility.CreateUserInfo(u)).ToList();
- UserUserInfo = mockUserInfos[0];
- AdminUserInfo = mockUserInfos[1];
- UserInfos = mockUserInfos;
+ return users;
}
- public const string UserUsername = "user";
- public const string AdminUsername = "admin";
- public const string UserPassword = "user";
- public const string AdminPassword = "admin";
-
- internal static IReadOnlyList<User> Users { get; }
public static IReadOnlyList<UserInfo> UserInfos { get; }
public static UserInfo AdminUserInfo { get; }
diff --git a/Timeline.Tests/UserAvatarServiceTest.cs b/Timeline.Tests/UserAvatarServiceTest.cs index 03b64a6b..f11da4f0 100644 --- a/Timeline.Tests/UserAvatarServiceTest.cs +++ b/Timeline.Tests/UserAvatarServiceTest.cs @@ -17,11 +17,15 @@ namespace Timeline.Tests {
public class MockDefaultUserAvatarProvider : IDefaultUserAvatarProvider
{
- public static Avatar Avatar { get; } = new Avatar { Type = "image/test", Data = Encoding.ASCII.GetBytes("test") };
+ public static AvatarInfo AvatarInfo { get; } = new AvatarInfo
+ {
+ Avatar = new Avatar { Type = "image/test", Data = Encoding.ASCII.GetBytes("test") },
+ LastModified = DateTime.Now
+ };
- public Task<Avatar> GetDefaultAvatar()
+ public Task<AvatarInfo> GetDefaultAvatar()
{
- return Task.FromResult(Avatar);
+ return Task.FromResult(AvatarInfo);
}
}
@@ -153,7 +157,7 @@ namespace Timeline.Tests public async Task GetAvatar_ShouldReturn_Default()
{
const string username = MockUsers.UserUsername;
- (await _service.GetAvatar(username)).Should().BeEquivalentTo(await _mockDefaultUserAvatarProvider.GetDefaultAvatar());
+ (await _service.GetAvatar(username)).Avatar.Should().BeEquivalentTo((await _mockDefaultUserAvatarProvider.GetDefaultAvatar()).Avatar);
}
[Fact]
@@ -173,7 +177,7 @@ namespace Timeline.Tests await context.SaveChangesAsync();
}
- (await _service.GetAvatar(username)).Should().BeEquivalentTo(MockAvatar);
+ (await _service.GetAvatar(username)).Avatar.Should().BeEquivalentTo(MockAvatar);
}
[Fact]
@@ -223,7 +227,8 @@ namespace Timeline.Tests // delete
await _service.SetAvatar(username, null);
- user.Avatar.Should().BeNull();
+ user.Avatar.Type.Should().BeNull();
+ user.Avatar.Data.Should().BeNull();
}
}
}
diff --git a/Timeline/Controllers/UserAvatarController.cs b/Timeline/Controllers/UserAvatarController.cs index 710ca764..89d2650c 100644 --- a/Timeline/Controllers/UserAvatarController.cs +++ b/Timeline/Controllers/UserAvatarController.cs @@ -57,10 +57,22 @@ namespace Timeline.Controllers [Authorize]
public async Task<IActionResult> Get(string username)
{
+ const string IfModifiedSinceHeaderKey = "If-Modified-Since";
try
{
- var avatar = await _service.GetAvatar(username);
- return File(avatar.Data, avatar.Type);
+ var avatarInfo = await _service.GetAvatar(username);
+ var avatar = avatarInfo.Avatar;
+ if (Request.Headers.TryGetValue(IfModifiedSinceHeaderKey, out var value))
+ {
+ var t = DateTime.Parse(value);
+ if (t > avatarInfo.LastModified)
+ {
+ Response.Headers.Add(IfModifiedSinceHeaderKey, avatarInfo.LastModified.ToString("r"));
+ return StatusCode(StatusCodes.Status304NotModified);
+ }
+ }
+
+ return File(avatar.Data, avatar.Type, new DateTimeOffset(avatarInfo.LastModified), null);
}
catch (UserNotExistException e)
{
diff --git a/Timeline/Entities/DatabaseContext.cs b/Timeline/Entities/DatabaseContext.cs index f32e5992..bc06b9df 100644 --- a/Timeline/Entities/DatabaseContext.cs +++ b/Timeline/Entities/DatabaseContext.cs @@ -28,6 +28,7 @@ namespace Timeline.Entities [Column("version"), Required]
public long Version { get; set; }
+ [Required]
public UserAvatar Avatar { get; set; }
}
@@ -42,7 +43,7 @@ namespace Timeline.Entities protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>().Property(e => e.Version).HasDefaultValue(0);
-
+ modelBuilder.Entity<User>().HasIndex(e => e.Name);
}
public DbSet<User> Users { get; set; }
diff --git a/Timeline/Entities/UserAvatar.cs b/Timeline/Entities/UserAvatar.cs index d7c24403..b941445d 100644 --- a/Timeline/Entities/UserAvatar.cs +++ b/Timeline/Entities/UserAvatar.cs @@ -1,4 +1,5 @@ -using System.ComponentModel.DataAnnotations;
+using System;
+using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Timeline.Entities
@@ -9,10 +10,26 @@ namespace Timeline.Entities [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { get; set; }
- [Column("data"), Required]
+ [Column("data")]
public byte[] Data { get; set; }
- [Column("type"), Required]
+ [Column("type")]
public string Type { get; set; }
+
+ [Column("last_modified"), Required]
+ public DateTime LastModified { get; set; }
+
+ public long UserId { get; set; }
+
+ public static UserAvatar Create(DateTime lastModified)
+ {
+ return new UserAvatar
+ {
+ Id = 0,
+ Data = null,
+ Type = null,
+ LastModified = lastModified
+ };
+ }
}
}
diff --git a/Timeline/Migrations/20190819074906_AddAvatarLastModified.Designer.cs b/Timeline/Migrations/20190819074906_AddAvatarLastModified.Designer.cs new file mode 100644 index 00000000..a6fe7941 --- /dev/null +++ b/Timeline/Migrations/20190819074906_AddAvatarLastModified.Designer.cs @@ -0,0 +1,86 @@ +// <auto-generated />
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Timeline.Entities;
+
+namespace Timeline.Migrations
+{
+ [DbContext(typeof(DatabaseContext))]
+ [Migration("20190819074906_AddAvatarLastModified")]
+ partial class AddAvatarLastModified
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "2.2.6-servicing-10079")
+ .HasAnnotation("Relational:MaxIdentifierLength", 64);
+
+ modelBuilder.Entity("Timeline.Entities.User", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id");
+
+ b.Property<string>("EncryptedPassword")
+ .IsRequired()
+ .HasColumnName("password");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasColumnName("name")
+ .HasMaxLength(26);
+
+ b.Property<string>("RoleString")
+ .IsRequired()
+ .HasColumnName("roles");
+
+ b.Property<long>("Version")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("version")
+ .HasDefaultValue(0L);
+
+ b.HasKey("Id");
+
+ b.ToTable("users");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserAvatar", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id");
+
+ b.Property<byte[]>("Data")
+ .HasColumnName("data");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnName("last_modified");
+
+ b.Property<string>("Type")
+ .HasColumnName("type");
+
+ b.Property<long>("UserId");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("user_avatars");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserAvatar", b =>
+ {
+ b.HasOne("Timeline.Entities.User")
+ .WithOne("Avatar")
+ .HasForeignKey("Timeline.Entities.UserAvatar", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Timeline/Migrations/20190819074906_AddAvatarLastModified.cs b/Timeline/Migrations/20190819074906_AddAvatarLastModified.cs new file mode 100644 index 00000000..d9b5e8cf --- /dev/null +++ b/Timeline/Migrations/20190819074906_AddAvatarLastModified.cs @@ -0,0 +1,114 @@ +using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+namespace Timeline.Migrations
+{
+ public partial class AddAvatarLastModified : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropForeignKey(
+ name: "FK_users_user_avatars_AvatarId",
+ table: "users");
+
+ migrationBuilder.DropIndex(
+ name: "IX_users_AvatarId",
+ table: "users");
+
+ migrationBuilder.DropColumn(
+ name: "AvatarId",
+ table: "users");
+
+ migrationBuilder.AlterColumn<string>(
+ name: "type",
+ table: "user_avatars",
+ nullable: true,
+ oldClrType: typeof(string));
+
+ migrationBuilder.AlterColumn<byte[]>(
+ name: "data",
+ table: "user_avatars",
+ nullable: true,
+ oldClrType: typeof(byte[]));
+
+ migrationBuilder.AddColumn<DateTime>(
+ name: "last_modified",
+ table: "user_avatars",
+ nullable: false,
+ defaultValue: DateTime.Now);
+
+ migrationBuilder.AddColumn<long>(
+ name: "UserId",
+ table: "user_avatars",
+ nullable: false,
+ defaultValue: 0L);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_user_avatars_UserId",
+ table: "user_avatars",
+ column: "UserId",
+ unique: true);
+
+ migrationBuilder.AddForeignKey(
+ name: "FK_user_avatars_users_UserId",
+ table: "user_avatars",
+ column: "UserId",
+ principalTable: "users",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+
+ // Note! Remember to manually create avatar entities for all users.
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropForeignKey(
+ name: "FK_user_avatars_users_UserId",
+ table: "user_avatars");
+
+ migrationBuilder.DropIndex(
+ name: "IX_user_avatars_UserId",
+ table: "user_avatars");
+
+ migrationBuilder.DropColumn(
+ name: "last_modified",
+ table: "user_avatars");
+
+ migrationBuilder.DropColumn(
+ name: "UserId",
+ table: "user_avatars");
+
+ migrationBuilder.AddColumn<long>(
+ name: "AvatarId",
+ table: "users",
+ nullable: true);
+
+ migrationBuilder.AlterColumn<string>(
+ name: "type",
+ table: "user_avatars",
+ nullable: false,
+ oldClrType: typeof(string),
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<byte[]>(
+ name: "data",
+ table: "user_avatars",
+ nullable: false,
+ oldClrType: typeof(byte[]),
+ oldNullable: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_users_AvatarId",
+ table: "users",
+ column: "AvatarId");
+
+ migrationBuilder.AddForeignKey(
+ name: "FK_users_user_avatars_AvatarId",
+ table: "users",
+ column: "AvatarId",
+ principalTable: "user_avatars",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Restrict);
+ }
+ }
+}
diff --git a/Timeline/Migrations/20190819080823_AddIndexForUserName.Designer.cs b/Timeline/Migrations/20190819080823_AddIndexForUserName.Designer.cs new file mode 100644 index 00000000..d45a057d --- /dev/null +++ b/Timeline/Migrations/20190819080823_AddIndexForUserName.Designer.cs @@ -0,0 +1,88 @@ +// <auto-generated />
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Timeline.Entities;
+
+namespace Timeline.Migrations
+{
+ [DbContext(typeof(DatabaseContext))]
+ [Migration("20190819080823_AddIndexForUserName")]
+ partial class AddIndexForUserName
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "2.2.6-servicing-10079")
+ .HasAnnotation("Relational:MaxIdentifierLength", 64);
+
+ modelBuilder.Entity("Timeline.Entities.User", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id");
+
+ b.Property<string>("EncryptedPassword")
+ .IsRequired()
+ .HasColumnName("password");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasColumnName("name")
+ .HasMaxLength(26);
+
+ b.Property<string>("RoleString")
+ .IsRequired()
+ .HasColumnName("roles");
+
+ b.Property<long>("Version")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("version")
+ .HasDefaultValue(0L);
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name");
+
+ b.ToTable("users");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserAvatar", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id");
+
+ b.Property<byte[]>("Data")
+ .HasColumnName("data");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnName("last_modified");
+
+ b.Property<string>("Type")
+ .HasColumnName("type");
+
+ b.Property<long>("UserId");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("user_avatars");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserAvatar", b =>
+ {
+ b.HasOne("Timeline.Entities.User")
+ .WithOne("Avatar")
+ .HasForeignKey("Timeline.Entities.UserAvatar", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Timeline/Migrations/20190819080823_AddIndexForUserName.cs b/Timeline/Migrations/20190819080823_AddIndexForUserName.cs new file mode 100644 index 00000000..b910a174 --- /dev/null +++ b/Timeline/Migrations/20190819080823_AddIndexForUserName.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations;
+
+namespace Timeline.Migrations
+{
+ public partial class AddIndexForUserName : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateIndex(
+ name: "IX_users_name",
+ table: "users",
+ column: "name");
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropIndex(
+ name: "IX_users_name",
+ table: "users");
+ }
+ }
+}
diff --git a/Timeline/Migrations/DatabaseContextModelSnapshot.cs b/Timeline/Migrations/DatabaseContextModelSnapshot.cs index 152bdea4..0eb85997 100644 --- a/Timeline/Migrations/DatabaseContextModelSnapshot.cs +++ b/Timeline/Migrations/DatabaseContextModelSnapshot.cs @@ -23,8 +23,6 @@ namespace Timeline.Migrations .ValueGeneratedOnAdd()
.HasColumnName("id");
- b.Property<long?>("AvatarId");
-
b.Property<string>("EncryptedPassword")
.IsRequired()
.HasColumnName("password");
@@ -45,7 +43,7 @@ namespace Timeline.Migrations b.HasKey("Id");
- b.HasIndex("AvatarId");
+ b.HasIndex("Name");
b.ToTable("users");
});
@@ -57,23 +55,30 @@ namespace Timeline.Migrations .HasColumnName("id");
b.Property<byte[]>("Data")
- .IsRequired()
.HasColumnName("data");
+ b.Property<DateTime>("LastModified")
+ .HasColumnName("last_modified");
+
b.Property<string>("Type")
- .IsRequired()
.HasColumnName("type");
+ b.Property<long>("UserId");
+
b.HasKey("Id");
+ b.HasIndex("UserId")
+ .IsUnique();
+
b.ToTable("user_avatars");
});
- modelBuilder.Entity("Timeline.Entities.User", b =>
+ modelBuilder.Entity("Timeline.Entities.UserAvatar", b =>
{
- b.HasOne("Timeline.Entities.UserAvatar", "Avatar")
- .WithMany()
- .HasForeignKey("AvatarId");
+ b.HasOne("Timeline.Entities.User")
+ .WithOne("Avatar")
+ .HasForeignKey("Timeline.Entities.UserAvatar", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
diff --git a/Timeline/Services/UserAvatarService.cs b/Timeline/Services/UserAvatarService.cs index dd0e5e7c..a83b8a52 100644 --- a/Timeline/Services/UserAvatarService.cs +++ b/Timeline/Services/UserAvatarService.cs @@ -18,6 +18,12 @@ namespace Timeline.Services public byte[] Data { get; set; }
}
+ public class AvatarInfo
+ {
+ public Avatar Avatar { get; set; }
+ public DateTime LastModified { get; set; }
+ }
+
/// <summary>
/// Thrown when avatar is of bad format.
/// </summary>
@@ -61,7 +67,7 @@ namespace Timeline.Services /// <summary>
/// Get the default avatar.
/// </summary>
- Task<Avatar> GetDefaultAvatar();
+ Task<AvatarInfo> GetDefaultAvatar();
}
public interface IUserAvatarValidator
@@ -80,10 +86,10 @@ namespace Timeline.Services /// Get avatar of a user. If the user has no avatar, a default one is returned.
/// </summary>
/// <param name="username">The username of the user to get avatar of.</param>
- /// <returns>The avatar.</returns>
+ /// <returns>The avatar info.</returns>
/// <exception cref="ArgumentException">Thrown if <paramref name="username"/> is null or empty.</exception>
/// <exception cref="UserNotExistException">Thrown if the user does not exist.</exception>
- Task<Avatar> GetAvatar(string username);
+ Task<AvatarInfo> GetAvatar(string username);
/// <summary>
/// Set avatar for a user.
@@ -106,12 +112,17 @@ namespace Timeline.Services _environment = environment;
}
- public async Task<Avatar> GetDefaultAvatar()
+ public async Task<AvatarInfo> GetDefaultAvatar()
{
- return new Avatar
+ var path = Path.Combine(_environment.ContentRootPath, "default-avatar.png");
+ return new AvatarInfo
{
- Type = "image/png",
- Data = await File.ReadAllBytesAsync(Path.Combine(_environment.ContentRootPath, "default-avatar.png"))
+ Avatar = new Avatar
+ {
+ Type = "image/png",
+ Data = await File.ReadAllBytesAsync(path)
+ },
+ LastModified = File.GetLastWriteTime(path)
};
}
}
@@ -134,7 +145,7 @@ namespace Timeline.Services }
catch (UnknownImageFormatException e)
{
- throw new AvatarDataException(avatar, AvatarDataException.ErrorReason.CantDecode, "Failed to decode image. See inner exception.", e);
+ throw new AvatarDataException(avatar, AvatarDataException.ErrorReason.CantDecode, "Failed to decode image. See inner exception.", e);
}
});
}
@@ -158,7 +169,7 @@ namespace Timeline.Services _avatarValidator = avatarValidator;
}
- public async Task<Avatar> GetAvatar(string username)
+ public async Task<AvatarInfo> GetAvatar(string username)
{
if (string.IsNullOrEmpty(username))
throw new ArgumentException("Username is null or empty.", nameof(username));
@@ -170,16 +181,26 @@ namespace Timeline.Services await _database.Entry(user).Reference(u => u.Avatar).LoadAsync();
var avatar = user.Avatar;
- if (avatar == null)
+ 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.");
+ // TODO: Throw an exception to indicate this.
+
+ if (avatar.Data == null)
{
- return await _defaultUserAvatarProvider.GetDefaultAvatar();
+ var defaultAvatar = await _defaultUserAvatarProvider.GetDefaultAvatar();
+ defaultAvatar.LastModified = defaultAvatar.LastModified > avatar.LastModified ? defaultAvatar.LastModified : avatar.LastModified;
+ return defaultAvatar;
}
else
{
- return new Avatar
+ return new AvatarInfo
{
- Type = avatar.Type,
- Data = avatar.Data
+ Avatar = new Avatar
+ {
+ Type = avatar.Type,
+ Data = avatar.Data
+ },
+ LastModified = avatar.LastModified
};
}
}
@@ -206,34 +227,25 @@ namespace Timeline.Services if (avatar == null)
{
- if (avatarEntity == null)
+ if (avatarEntity.Data == null)
return;
else
{
- _database.UserAvatars.Remove(avatarEntity);
+ avatarEntity.Data = null;
+ avatarEntity.Type = null;
+ avatarEntity.LastModified = DateTime.Now;
await _database.SaveChangesAsync();
- _logger.LogInformation("Removed an entry in user_avatars.");
+ _logger.LogInformation("Updated an entry in user_avatars.");
}
}
else
{
await _avatarValidator.Validate(avatar);
-
- if (avatarEntity == null)
- {
- user.Avatar = new UserAvatar
- {
- Type = avatar.Type,
- Data = avatar.Data
- };
- }
- else
- {
- avatarEntity.Type = avatar.Type;
- avatarEntity.Data = avatar.Data;
- }
+ avatarEntity.Type = avatar.Type;
+ avatarEntity.Data = avatar.Data;
+ avatarEntity.LastModified = DateTime.Now;
await _database.SaveChangesAsync();
- _logger.LogInformation("Added or modified an entry in user_avatars.");
+ _logger.LogInformation("Updated an entry in user_avatars.");
}
}
}
diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs index 96c3e256..347b8cbb 100644 --- a/Timeline/Services/UserService.cs +++ b/Timeline/Services/UserService.cs @@ -376,7 +376,7 @@ namespace Timeline.Services Name = username,
EncryptedPassword = _passwordService.HashPassword(password),
RoleString = IsAdminToRoleString(administrator),
- Version = 0
+ Avatar = UserAvatar.Create(DateTime.Now)
};
await _databaseContext.AddAsync(newUser);
await _databaseContext.SaveChangesAsync();
|