aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author杨宇千 <crupest@outlook.com>2019-10-24 16:56:41 +0800
committer杨宇千 <crupest@outlook.com>2019-10-24 16:56:41 +0800
commitc324d1dad0ffc1a1013b22792078415e7a50c470 (patch)
treec8f5032f86d8a9e5df0117d438ea741cb0a4f613
parent9a163719b76958374d1c27616393368e54e8b8a5 (diff)
downloadtimeline-c324d1dad0ffc1a1013b22792078415e7a50c470.tar.gz
timeline-c324d1dad0ffc1a1013b22792078415e7a50c470.tar.bz2
timeline-c324d1dad0ffc1a1013b22792078415e7a50c470.zip
...
-rw-r--r--Timeline.Tests/IntegratedTests/UserAvatarTest.cs (renamed from Timeline.Tests/IntegratedTests/UserAvatarTests.cs)24
-rw-r--r--Timeline.Tests/UserAvatarServiceTest.cs12
-rw-r--r--Timeline/Controllers/UserAvatarController.cs135
-rw-r--r--Timeline/Entities/UserAvatar.cs1
-rw-r--r--Timeline/ErrorCodes.cs15
-rw-r--r--Timeline/Filters/ContentHeaderAttributes.cs13
-rw-r--r--Timeline/Helpers/LanguageHelper.cs12
-rw-r--r--Timeline/Helpers/Log.cs20
-rw-r--r--Timeline/Models/Http/Common.cs66
-rw-r--r--Timeline/Resources/Controllers/UserAvatarController.Designer.cs171
-rw-r--r--Timeline/Resources/Controllers/UserAvatarController.en.resx144
-rw-r--r--Timeline/Resources/Controllers/UserAvatarController.resx156
-rw-r--r--Timeline/Resources/Controllers/UserAvatarController.zh.resx144
-rw-r--r--Timeline/Resources/Models/Http/Common.en.resx29
-rw-r--r--Timeline/Resources/Models/Http/Common.zh.resx29
-rw-r--r--Timeline/Resources/Services/Exception.Designer.cs45
-rw-r--r--Timeline/Resources/Services/Exception.resx15
-rw-r--r--Timeline/Resources/Services/UserAvatarService.Designer.cs108
-rw-r--r--Timeline/Resources/Services/UserAvatarService.resx135
-rw-r--r--Timeline/Services/AvatarFormatException.cs51
-rw-r--r--Timeline/Services/DatabaseExtensions.cs15
-rw-r--r--Timeline/Services/ETagGenerator.cs17
-rw-r--r--Timeline/Services/UserAvatarService.cs187
-rw-r--r--Timeline/Services/UserService.cs2
-rw-r--r--Timeline/Timeline.csproj18
25 files changed, 1331 insertions, 233 deletions
diff --git a/Timeline.Tests/IntegratedTests/UserAvatarTests.cs b/Timeline.Tests/IntegratedTests/UserAvatarTest.cs
index ad0e4221..ba6d98e1 100644
--- a/Timeline.Tests/IntegratedTests/UserAvatarTests.cs
+++ b/Timeline.Tests/IntegratedTests/UserAvatarTest.cs
@@ -18,6 +18,8 @@ using Timeline.Services;
using Timeline.Tests.Helpers;
using Timeline.Tests.Helpers.Authentication;
using Xunit;
+using static Timeline.ErrorCodes.Http.Common;
+using static Timeline.ErrorCodes.Http.UserAvatar;
namespace Timeline.Tests.IntegratedTests
{
@@ -52,7 +54,7 @@ namespace Timeline.Tests.IntegratedTests
var res = await client.GetAsync("users/usernotexist/avatar");
res.Should().HaveStatusCode(404)
.And.Should().HaveCommonBody()
- .Which.Code.Should().Be(UserAvatarController.ErrorCodes.Get_UserNotExist);
+ .Which.Code.Should().Be(Get.UserNotExist);
}
var env = _factory.Server.Host.Services.GetRequiredService<IWebHostEnvironment>();
@@ -153,7 +155,7 @@ namespace Timeline.Tests.IntegratedTests
content.Headers.ContentType = new MediaTypeHeaderValue("image/png");
var res = await client.PutAsync("users/user/avatar", content);
res.Should().HaveStatusCode(HttpStatusCode.BadRequest)
- .And.Should().HaveCommonBody().Which.Code.Should().Be(UserAvatarController.ErrorCodes.Put_Content_TooBig);
+ .And.Should().HaveCommonBody().Which.Code.Should().Be(Content.TooBig);
}
{
@@ -162,7 +164,7 @@ namespace Timeline.Tests.IntegratedTests
content.Headers.ContentType = new MediaTypeHeaderValue("image/png");
var res = await client.PutAsync("users/user/avatar", content);
res.Should().HaveStatusCode(HttpStatusCode.BadRequest)
- .And.Should().HaveCommonBody().Which.Code.Should().Be(UserAvatarController.ErrorCodes.Put_Content_UnmatchedLength_Less);
+ .And.Should().HaveCommonBody().Which.Code.Should().Be(Content.UnmatchedLength_Smaller);
}
{
@@ -171,25 +173,25 @@ namespace Timeline.Tests.IntegratedTests
content.Headers.ContentType = new MediaTypeHeaderValue("image/png");
var res = await client.PutAsync("users/user/avatar", content);
res.Should().HaveStatusCode(HttpStatusCode.BadRequest)
- .And.Should().HaveCommonBody().Which.Code.Should().Be(UserAvatarController.ErrorCodes.Put_Content_UnmatchedLength_Bigger);
+ .And.Should().HaveCommonBody().Which.Code.Should().Be(Content.UnmatchedLength_Bigger);
}
{
var res = await client.PutByteArrayAsync("users/user/avatar", new[] { (byte)0x00 }, "image/png");
res.Should().HaveStatusCode(HttpStatusCode.BadRequest)
- .And.Should().HaveCommonBody().Which.Code.Should().Be(UserAvatarController.ErrorCodes.Put_BadFormat_CantDecode);
+ .And.Should().HaveCommonBody().Which.Code.Should().Be(Put.BadFormat_CantDecode);
}
{
var res = await client.PutByteArrayAsync("users/user/avatar", mockAvatar.Data, "image/jpeg");
res.Should().HaveStatusCode(HttpStatusCode.BadRequest)
- .And.Should().HaveCommonBody().Which.Code.Should().Be(UserAvatarController.ErrorCodes.Put_BadFormat_UnmatchedFormat);
+ .And.Should().HaveCommonBody().Which.Code.Should().Be(Put.BadFormat_UnmatchedFormat);
}
{
var res = await client.PutByteArrayAsync("users/user/avatar", ImageHelper.CreatePngWithSize(100, 200), "image/png");
res.Should().HaveStatusCode(HttpStatusCode.BadRequest)
- .And.Should().HaveCommonBody().Which.Code.Should().Be(UserAvatarController.ErrorCodes.Put_BadFormat_BadSize);
+ .And.Should().HaveCommonBody().Which.Code.Should().Be(Put.BadFormat_BadSize);
}
{
@@ -219,13 +221,13 @@ namespace Timeline.Tests.IntegratedTests
{
var res = await client.PutByteArrayAsync("users/admin/avatar", new[] { (byte)0x00 }, "image/png");
res.Should().HaveStatusCode(HttpStatusCode.Forbidden)
- .And.Should().HaveCommonBody().Which.Code.Should().Be(UserAvatarController.ErrorCodes.Put_Forbid);
+ .And.Should().HaveCommonBody().Which.Code.Should().Be(Put.Forbid);
}
{
var res = await client.DeleteAsync("users/admin/avatar");
res.Should().HaveStatusCode(HttpStatusCode.Forbidden)
- .And.Should().HaveCommonBody().Which.Code.Should().Be(UserAvatarController.ErrorCodes.Delete_Forbid);
+ .And.Should().HaveCommonBody().Which.Code.Should().Be(Delete.Forbid);
}
for (int i = 0; i < 2; i++) // double delete should work.
@@ -253,13 +255,13 @@ namespace Timeline.Tests.IntegratedTests
var res = await client.PutByteArrayAsync("users/usernotexist/avatar", new[] { (byte)0x00 }, "image/png");
res.Should().HaveStatusCode(400)
.And.Should().HaveCommonBody()
- .Which.Code.Should().Be(UserAvatarController.ErrorCodes.Put_UserNotExist);
+ .Which.Code.Should().Be(Put.UserNotExist);
}
{
var res = await client.DeleteAsync("users/usernotexist/avatar");
res.Should().HaveStatusCode(400)
- .And.Should().HaveCommonBody().Which.Code.Should().Be(UserAvatarController.ErrorCodes.Delete_UserNotExist);
+ .And.Should().HaveCommonBody().Which.Code.Should().Be(Delete.UserNotExist);
}
}
}
diff --git a/Timeline.Tests/UserAvatarServiceTest.cs b/Timeline.Tests/UserAvatarServiceTest.cs
index d22ad113..7489517b 100644
--- a/Timeline.Tests/UserAvatarServiceTest.cs
+++ b/Timeline.Tests/UserAvatarServiceTest.cs
@@ -63,8 +63,8 @@ namespace Timeline.Tests
Type = "image/png"
};
_validator.Awaiting(v => v.Validate(avatar))
- .Should().Throw<AvatarDataException>()
- .Where(e => e.Avatar == avatar && e.Error == AvatarDataException.ErrorReason.CantDecode);
+ .Should().Throw<AvatarFormatException>()
+ .Where(e => e.Avatar == avatar && e.Error == AvatarFormatException.ErrorReason.CantDecode);
}
[Fact]
@@ -76,8 +76,8 @@ namespace Timeline.Tests
Type = "image/jpeg"
};
_validator.Awaiting(v => v.Validate(avatar))
- .Should().Throw<AvatarDataException>()
- .Where(e => e.Avatar == avatar && e.Error == AvatarDataException.ErrorReason.UnmatchedFormat);
+ .Should().Throw<AvatarFormatException>()
+ .Where(e => e.Avatar == avatar && e.Error == AvatarFormatException.ErrorReason.UnmatchedFormat);
}
[Fact]
@@ -89,8 +89,8 @@ namespace Timeline.Tests
Type = PngFormat.Instance.DefaultMimeType
};
_validator.Awaiting(v => v.Validate(avatar))
- .Should().Throw<AvatarDataException>()
- .Where(e => e.Avatar == avatar && e.Error == AvatarDataException.ErrorReason.BadSize);
+ .Should().Throw<AvatarFormatException>()
+ .Where(e => e.Avatar == avatar && e.Error == AvatarFormatException.ErrorReason.BadSize);
}
[Fact]
diff --git a/Timeline/Controllers/UserAvatarController.cs b/Timeline/Controllers/UserAvatarController.cs
index 5cba1d93..838a3928 100644
--- a/Timeline/Controllers/UserAvatarController.cs
+++ b/Timeline/Controllers/UserAvatarController.cs
@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
using System;
@@ -8,61 +9,67 @@ using System.Linq;
using System.Threading.Tasks;
using Timeline.Authentication;
using Timeline.Filters;
+using Timeline.Helpers;
using Timeline.Models.Http;
+using Timeline.Models.Validation;
using Timeline.Services;
-namespace Timeline.Controllers
+namespace Timeline
{
- [ApiController]
- public class UserAvatarController : Controller
+ public static partial class ErrorCodes
{
- public static class ErrorCodes
+ public static partial class Http
{
- public const int Get_UserNotExist = -1001;
-
- public const int Put_UserNotExist = -2001;
- public const int Put_Forbid = -2002;
- public const int Put_BadFormat_CantDecode = -2011;
- public const int Put_BadFormat_UnmatchedFormat = -2012;
- public const int Put_BadFormat_BadSize = -2013;
- public const int Put_Content_TooBig = -2021;
- public const int Put_Content_UnmatchedLength_Less = -2022;
- public const int Put_Content_UnmatchedLength_Bigger = -2023;
+ public static class UserAvatar // bbb = 003
+ {
+ public static class Get // cc = 01
+ {
+ public const int UserNotExist = 10030101;
+ }
- public const int Delete_UserNotExist = -3001;
- public const int Delete_Forbid = -3002;
+ public static class Put // cc = 02
+ {
+ public const int UserNotExist = 10030201;
+ public const int Forbid = 10030202;
+ public const int BadFormat_CantDecode = 10030203;
+ public const int BadFormat_UnmatchedFormat = 10030204;
+ public const int BadFormat_BadSize = 10030205;
+ }
- public static int From(AvatarDataException.ErrorReason error)
- {
- switch (error)
+ public static class Delete // cc = 03
{
- case AvatarDataException.ErrorReason.CantDecode:
- return Put_BadFormat_CantDecode;
- case AvatarDataException.ErrorReason.UnmatchedFormat:
- return Put_BadFormat_UnmatchedFormat;
- case AvatarDataException.ErrorReason.BadSize:
- return Put_BadFormat_BadSize;
- default:
- throw new Exception("Unknown AvatarDataException.ErrorReason value.");
+ public const int UserNotExist = 10030301;
+ public const int Forbid = 10030302;
}
}
}
+ }
+}
+namespace Timeline.Controllers
+{
+ [ApiController]
+ public class UserAvatarController : Controller
+ {
private readonly ILogger<UserAvatarController> _logger;
private readonly IUserAvatarService _service;
- public UserAvatarController(ILogger<UserAvatarController> logger, IUserAvatarService service)
+ private readonly IStringLocalizerFactory _localizerFactory;
+ private readonly IStringLocalizer<UserAvatarController> _localizer;
+
+ public UserAvatarController(ILogger<UserAvatarController> logger, IUserAvatarService service, IStringLocalizerFactory localizerFactory)
{
_logger = logger;
_service = service;
+ _localizerFactory = localizerFactory;
+ _localizer = new StringLocalizer<UserAvatarController>(localizerFactory);
}
[HttpGet("users/{username}/avatar")]
- [Authorize]
[ResponseCache(NoStore = false, Location = ResponseCacheLocation.None, Duration = 0)]
- public async Task<IActionResult> Get([FromRoute] string username)
+ public async Task<IActionResult> Get([FromRoute][Username] string username)
{
const string IfNonMatchHeaderKey = "If-None-Match";
@@ -74,11 +81,16 @@ namespace Timeline.Controllers
if (Request.Headers.TryGetValue(IfNonMatchHeaderKey, out var value))
{
if (!EntityTagHeaderValue.TryParseStrictList(value, out var eTagList))
- return BadRequest(CommonResponse.BadIfNonMatch());
+ {
+ _logger.LogInformation(Log.Format(Resources.Controllers.UserAvatarController.LogGetBadIfNoneMatch,
+ ("Username", username), ("If-None-Match", value)));
+ return BadRequest(HeaderErrorResponse.BadIfNonMatch(_localizerFactory));
+ }
if (eTagList.FirstOrDefault(e => e.Equals(eTag)) != null)
{
Response.Headers.Add("ETag", eTagValue);
+ _logger.LogInformation(Log.Format(Resources.Controllers.UserAvatarController.LogGetReturnNotModify, ("Username", username)));
return StatusCode(StatusCodes.Status304NotModified);
}
}
@@ -86,12 +98,13 @@ namespace Timeline.Controllers
var avatarInfo = await _service.GetAvatar(username);
var avatar = avatarInfo.Avatar;
+ _logger.LogInformation(Log.Format(Resources.Controllers.UserAvatarController.LogGetReturnData, ("Username", username)));
return File(avatar.Data, avatar.Type, new DateTimeOffset(avatarInfo.LastModified), eTag);
}
catch (UserNotExistException e)
{
- _logger.LogInformation(e, $"Attempt to get a avatar of a non-existent user failed. Username: {username} .");
- return NotFound(new CommonResponse(ErrorCodes.Get_UserNotExist, "User does not exist."));
+ _logger.LogInformation(e, Log.Format(Resources.Controllers.UserAvatarController.LogGetUserNotExist, ("Username", username)));
+ return NotFound(new CommonResponse(ErrorCodes.Http.UserAvatar.Get.UserNotExist, _localizer["ErrorGetUserNotExist"]));
}
}
@@ -99,18 +112,18 @@ namespace Timeline.Controllers
[Authorize]
[RequireContentType, RequireContentLength]
[Consumes("image/png", "image/jpeg", "image/gif", "image/webp")]
- public async Task<IActionResult> Put(string username)
+ public async Task<IActionResult> Put([FromRoute][Username] string username)
{
- var contentLength = Request.ContentLength.Value;
+ var contentLength = Request.ContentLength!.Value;
if (contentLength > 1000 * 1000 * 10)
- return BadRequest(new CommonResponse(ErrorCodes.Put_Content_TooBig,
- "Content can't be bigger than 10MB."));
+ return BadRequest(ContentErrorResponse.TooBig(_localizerFactory, "10MB"));
if (!User.IsAdministrator() && User.Identity.Name != username)
{
- _logger.LogInformation($"Attempt to put a avatar of other user as a non-admin failed. Operator Username: {User.Identity.Name} ; Username To Put Avatar: {username} .");
+ _logger.LogInformation(Log.Format(Resources.Controllers.UserAvatarController.LogPutForbid,
+ ("Operator Username", User.Identity.Name), ("Username To Put Avatar", username)));
return StatusCode(StatusCodes.Status403Forbidden,
- new CommonResponse(ErrorCodes.Put_Forbid, "Normal user can't change other's avatar."));
+ new CommonResponse(ErrorCodes.Http.UserAvatar.Put.Forbid, _localizer["ErrorPutForbid"]));
}
try
@@ -119,13 +132,11 @@ namespace Timeline.Controllers
var bytesRead = await Request.Body.ReadAsync(data);
if (bytesRead != contentLength)
- return BadRequest(new CommonResponse(ErrorCodes.Put_Content_UnmatchedLength_Less,
- $"Content length in header is {contentLength} but actual length is {bytesRead}."));
+ return BadRequest(ContentErrorResponse.UnmatchedLength_Smaller(_localizerFactory));
var extraByte = new byte[1];
if (await Request.Body.ReadAsync(extraByte) != 0)
- return BadRequest(new CommonResponse(ErrorCodes.Put_Content_UnmatchedLength_Bigger,
- $"Content length in header is {contentLength} but actual length is bigger than that."));
+ return BadRequest(ContentErrorResponse.UnmatchedLength_Bigger(_localizerFactory));
await _service.SetAvatar(username, new Avatar
{
@@ -133,43 +144,57 @@ namespace Timeline.Controllers
Type = Request.ContentType
});
- _logger.LogInformation($"Succeed to put a avatar of a user. Username: {username} ; Mime Type: {Request.ContentType} .");
+ _logger.LogInformation(Log.Format(Resources.Controllers.UserAvatarController.LogPutSuccess,
+ ("Username", username), ("Mime Type", Request.ContentType)));
return Ok();
}
catch (UserNotExistException e)
{
- _logger.LogInformation(e, $"Attempt to put a avatar of a non-existent user failed. Username: {username} .");
- return BadRequest(new CommonResponse(ErrorCodes.Put_UserNotExist, "User does not exist."));
+ _logger.LogInformation(e, Log.Format(Resources.Controllers.UserAvatarController.LogPutUserNotExist, ("Username", username)));
+ return BadRequest(new CommonResponse(ErrorCodes.Http.UserAvatar.Put.UserNotExist, _localizer["ErrorPutUserNotExist"]));
}
- catch (AvatarDataException e)
+ catch (AvatarFormatException e)
{
- _logger.LogInformation(e, $"Attempt to put a avatar of a bad format failed. Username: {username} .");
- return BadRequest(new CommonResponse(ErrorCodes.From(e.Error), "Bad format."));
+ var (code, message) = e.Error switch
+ {
+ AvatarFormatException.ErrorReason.CantDecode =>
+ (ErrorCodes.Http.UserAvatar.Put.BadFormat_CantDecode, _localizer["ErrorPutBadFormatCantDecode"]),
+ AvatarFormatException.ErrorReason.UnmatchedFormat =>
+ (ErrorCodes.Http.UserAvatar.Put.BadFormat_UnmatchedFormat, _localizer["ErrorPutBadFormatUnmatchedFormat"]),
+ AvatarFormatException.ErrorReason.BadSize =>
+ (ErrorCodes.Http.UserAvatar.Put.BadFormat_BadSize, _localizer["ErrorPutBadFormatBadSize"]),
+ _ =>
+ throw new Exception(Resources.Controllers.UserAvatarController.ExceptionUnknownAvatarFormatError)
+ };
+
+ _logger.LogInformation(e, Log.Format(Resources.Controllers.UserAvatarController.LogPutUserBadFormat, ("Username", username)));
+ return BadRequest(new CommonResponse(code, message));
}
}
[HttpDelete("users/{username}/avatar")]
[Authorize]
- public async Task<IActionResult> Delete([FromRoute] string username)
+ public async Task<IActionResult> Delete([FromRoute][Username] string username)
{
if (!User.IsAdministrator() && User.Identity.Name != username)
{
- _logger.LogInformation($"Attempt to delete a avatar of other user as a non-admin failed. Operator Username: {User.Identity.Name} ; Username To Put Avatar: {username} .");
+ _logger.LogInformation(Log.Format(Resources.Controllers.UserAvatarController.LogPutUserBadFormat,
+ ("Operator Username", User.Identity.Name), ("Username To Delete Avatar", username)));
return StatusCode(StatusCodes.Status403Forbidden,
- new CommonResponse(ErrorCodes.Delete_Forbid, "Normal user can't delete other's avatar."));
+ new CommonResponse(ErrorCodes.Http.UserAvatar.Delete.Forbid, _localizer["ErrorDeleteForbid"]));
}
try
{
await _service.SetAvatar(username, null);
- _logger.LogInformation($"Succeed to delete a avatar of a user. Username: {username} .");
+ _logger.LogInformation(Log.Format(Resources.Controllers.UserAvatarController.LogDeleteSuccess, ("Username", username)));
return Ok();
}
catch (UserNotExistException e)
{
- _logger.LogInformation(e, $"Attempt to delete a avatar of a non-existent user failed. Username: {username} .");
- return BadRequest(new CommonResponse(ErrorCodes.Delete_UserNotExist, "User does not exist."));
+ _logger.LogInformation(e, Log.Format(Resources.Controllers.UserAvatarController.LogDeleteNotExist, ("Username", username)));
+ return BadRequest(new CommonResponse(ErrorCodes.Http.UserAvatar.Delete.UserNotExist, _localizer["ErrorDeleteUserNotExist"]));
}
}
}
diff --git a/Timeline/Entities/UserAvatar.cs b/Timeline/Entities/UserAvatar.cs
index d47bb28b..3b5388aa 100644
--- a/Timeline/Entities/UserAvatar.cs
+++ b/Timeline/Entities/UserAvatar.cs
@@ -11,6 +11,7 @@ namespace Timeline.Entities
public long Id { get; set; }
[Column("data")]
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1819:Properties should not return arrays", Justification = "This is data base entity.")]
public byte[]? Data { get; set; }
[Column("type")]
diff --git a/Timeline/ErrorCodes.cs b/Timeline/ErrorCodes.cs
index 0b325e27..5e7f003a 100644
--- a/Timeline/ErrorCodes.cs
+++ b/Timeline/ErrorCodes.cs
@@ -17,10 +17,17 @@
public static class Header // cc = 01
{
- public const int Missing_ContentType = 10010101; // dd = 01
- public const int Missing_ContentLength = 10010102; // dd = 02
- public const int Zero_ContentLength = 10010103; // dd = 03
- public const int BadFormat_IfNonMatch = 10010104; // dd = 04
+ public const int Missing_ContentType = 10000101; // dd = 01
+ public const int Missing_ContentLength = 10000102; // dd = 02
+ public const int Zero_ContentLength = 10000103; // dd = 03
+ public const int BadFormat_IfNonMatch = 10000104; // dd = 04
+ }
+
+ public static class Content // cc = 02
+ {
+ public const int TooBig = 1000201;
+ public const int UnmatchedLength_Smaller = 10030202;
+ public const int UnmatchedLength_Bigger = 10030203;
}
}
}
diff --git a/Timeline/Filters/ContentHeaderAttributes.cs b/Timeline/Filters/ContentHeaderAttributes.cs
index 14685a01..e3d4eeb2 100644
--- a/Timeline/Filters/ContentHeaderAttributes.cs
+++ b/Timeline/Filters/ContentHeaderAttributes.cs
@@ -1,16 +1,20 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Localization;
using Timeline.Models.Http;
namespace Timeline.Filters
{
public class RequireContentTypeAttribute : ActionFilterAttribute
{
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods")]
public override void OnActionExecuting(ActionExecutingContext context)
{
if (context.HttpContext.Request.ContentType == null)
{
- context.Result = new BadRequestObjectResult(CommonResponse.MissingContentType());
+ var localizerFactory = context.HttpContext.RequestServices.GetRequiredService<IStringLocalizerFactory>();
+ context.Result = new BadRequestObjectResult(HeaderErrorResponse.MissingContentType(localizerFactory));
}
}
}
@@ -30,17 +34,20 @@ namespace Timeline.Filters
public bool RequireNonZero { get; set; }
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods")]
public override void OnActionExecuting(ActionExecutingContext context)
{
if (context.HttpContext.Request.ContentLength == null)
{
- context.Result = new BadRequestObjectResult(CommonResponse.MissingContentLength());
+ var localizerFactory = context.HttpContext.RequestServices.GetRequiredService<IStringLocalizerFactory>();
+ context.Result = new BadRequestObjectResult(HeaderErrorResponse.MissingContentLength(localizerFactory));
return;
}
if (RequireNonZero && context.HttpContext.Request.ContentLength.Value == 0)
{
- context.Result = new BadRequestObjectResult(CommonResponse.ZeroContentLength());
+ var localizerFactory = context.HttpContext.RequestServices.GetRequiredService<IStringLocalizerFactory>();
+ context.Result = new BadRequestObjectResult(HeaderErrorResponse.ZeroContentLength(localizerFactory));
return;
}
}
diff --git a/Timeline/Helpers/LanguageHelper.cs b/Timeline/Helpers/LanguageHelper.cs
new file mode 100644
index 00000000..b0156b8b
--- /dev/null
+++ b/Timeline/Helpers/LanguageHelper.cs
@@ -0,0 +1,12 @@
+using System.Linq;
+
+namespace Timeline.Helpers
+{
+ public static class LanguageHelper
+ {
+ public static bool AreSame(this bool firstBool, params bool[] otherBools)
+ {
+ return otherBools.All(b => b == firstBool);
+ }
+ }
+}
diff --git a/Timeline/Helpers/Log.cs b/Timeline/Helpers/Log.cs
index 8deebf1d..68c975fa 100644
--- a/Timeline/Helpers/Log.cs
+++ b/Timeline/Helpers/Log.cs
@@ -3,26 +3,6 @@ using System.Text;
namespace Timeline.Helpers
{
- // TODO! Remember to remove this after refactor.
- public static class MyLogHelper
- {
- public static KeyValuePair<string, object> Pair(string key, object value) => new KeyValuePair<string, object>(key, value);
-
- public static string FormatLogMessage(string summary, params KeyValuePair<string, object>[] properties)
- {
- var builder = new StringBuilder();
- builder.Append(summary);
- foreach (var property in properties)
- {
- builder.AppendLine();
- builder.Append(property.Key);
- builder.Append(" : ");
- builder.Append(property.Value);
- }
- return builder.ToString();
- }
- }
-
public static class Log
{
public static string Format(string summary, params (string, object?)[] properties)
diff --git a/Timeline/Models/Http/Common.cs b/Timeline/Models/Http/Common.cs
index c741837a..39ddddd9 100644
--- a/Timeline/Models/Http/Common.cs
+++ b/Timeline/Models/Http/Common.cs
@@ -5,46 +5,74 @@ namespace Timeline.Models.Http
{
public class CommonResponse
{
- public static CommonResponse InvalidModel(string message)
+ internal static CommonResponse InvalidModel(string message)
{
return new CommonResponse(ErrorCodes.Http.Common.InvalidModel, message);
}
- public static CommonResponse MissingContentType()
+ public CommonResponse()
{
- return new CommonResponse(ErrorCodes.Http.Common.Header.Missing_ContentType, "Header Content-Type is required.");
+
}
- public static CommonResponse MissingContentLength()
+ public CommonResponse(int code, string message)
{
- return new CommonResponse(ErrorCodes.Http.Common.Header.Missing_ContentLength, "Header Content-Length is missing or of bad format.");
+ Code = code;
+ Message = message;
}
- public static CommonResponse ZeroContentLength()
+ public int Code { get; set; }
+ public string? Message { get; set; }
+ }
+
+ internal static class HeaderErrorResponse
+ {
+ internal static CommonResponse MissingContentType(IStringLocalizerFactory localizerFactory)
{
- return new CommonResponse(ErrorCodes.Http.Common.Header.Zero_ContentLength, "Header Content-Length must not be 0.");
+ var localizer = localizerFactory.Create("Models.Http.Common");
+ return new CommonResponse(ErrorCodes.Http.Common.Header.Missing_ContentType, localizer["HeaderMissingContentType"]);
}
- public static CommonResponse BadIfNonMatch()
+ internal static CommonResponse MissingContentLength(IStringLocalizerFactory localizerFactory)
{
- return new CommonResponse(ErrorCodes.Http.Common.Header.BadFormat_IfNonMatch, "Header If-Non-Match is of bad format.");
+ var localizer = localizerFactory.Create("Models.Http.Common");
+ return new CommonResponse(ErrorCodes.Http.Common.Header.Missing_ContentLength, localizer["HeaderMissingContentLength"]);
}
- public CommonResponse()
+ internal static CommonResponse ZeroContentLength(IStringLocalizerFactory localizerFactory)
{
+ var localizer = localizerFactory.Create("Models.Http.Common");
+ return new CommonResponse(ErrorCodes.Http.Common.Header.Zero_ContentLength, localizer["HeaderZeroContentLength"]);
+ }
+ internal static CommonResponse BadIfNonMatch(IStringLocalizerFactory localizerFactory)
+ {
+ var localizer = localizerFactory.Create("Models.Http.Common");
+ return new CommonResponse(ErrorCodes.Http.Common.Header.BadFormat_IfNonMatch, localizer["HeaderBadIfNonMatch"]);
}
+ }
- public CommonResponse(int code, string message)
+ internal static class ContentErrorResponse
+ {
+ internal static CommonResponse TooBig(IStringLocalizerFactory localizerFactory, string maxLength)
{
- Code = code;
- Message = message;
+ var localizer = localizerFactory.Create("Models.Http.Common");
+ return new CommonResponse(ErrorCodes.Http.Common.Content.TooBig, localizer["ContentTooBig", maxLength]);
}
- public int Code { get; set; }
- public string? Message { get; set; }
+ internal static CommonResponse UnmatchedLength_Smaller(IStringLocalizerFactory localizerFactory)
+ {
+ var localizer = localizerFactory.Create("Models.Http.Common");
+ return new CommonResponse(ErrorCodes.Http.Common.Content.UnmatchedLength_Smaller, localizer["ContentUnmatchedLengthSmaller"]);
+ }
+ internal static CommonResponse UnmatchedLength_Bigger(IStringLocalizerFactory localizerFactory)
+ {
+ var localizer = localizerFactory.Create("Models.Http.Common");
+ return new CommonResponse(ErrorCodes.Http.Common.Content.UnmatchedLength_Bigger, localizer["ContentUnmatchedLengthBigger"]);
+ }
}
+
public class CommonDataResponse<T> : CommonResponse
{
public CommonDataResponse()
@@ -87,13 +115,13 @@ namespace Timeline.Models.Http
internal static CommonPutResponse Create(IStringLocalizerFactory localizerFactory)
{
var localizer = localizerFactory.Create("Models.Http.Common");
- return new CommonPutResponse(0, localizer["ResponsePutCreate"], true);
+ return new CommonPutResponse(0, localizer["PutCreate"], true);
}
internal static CommonPutResponse Modify(IStringLocalizerFactory localizerFactory)
{
var localizer = localizerFactory.Create("Models.Http.Common");
- return new CommonPutResponse(0, localizer["ResponsePutModify"], false);
+ return new CommonPutResponse(0, localizer["PutModify"], false);
}
}
@@ -124,13 +152,13 @@ namespace Timeline.Models.Http
internal static CommonDeleteResponse Delete(IStringLocalizerFactory localizerFactory)
{
var localizer = localizerFactory.Create("Models.Http.Common");
- return new CommonDeleteResponse(0, localizer["ResponseDeleteDelete"], true);
+ return new CommonDeleteResponse(0, localizer["DeleteDelete"], true);
}
internal static CommonDeleteResponse NotExist(IStringLocalizerFactory localizerFactory)
{
var localizer = localizerFactory.Create("Models.Models.Http.Common");
- return new CommonDeleteResponse(0, localizer["ResponseDeleteNotExist"], false);
+ return new CommonDeleteResponse(0, localizer["DeleteNotExist"], false);
}
}
}
diff --git a/Timeline/Resources/Controllers/UserAvatarController.Designer.cs b/Timeline/Resources/Controllers/UserAvatarController.Designer.cs
new file mode 100644
index 00000000..e6eeb1e8
--- /dev/null
+++ b/Timeline/Resources/Controllers/UserAvatarController.Designer.cs
@@ -0,0 +1,171 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Timeline.Resources.Controllers {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class UserAvatarController {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal UserAvatarController() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Controllers.UserAvatarController", typeof(UserAvatarController).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Unknown AvatarDataException.ErrorReason value..
+ /// </summary>
+ internal static string ExceptionUnknownAvatarFormatError {
+ get {
+ return ResourceManager.GetString("ExceptionUnknownAvatarFormatError", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Attempt to delete a avatar of other user as a non-admin failed..
+ /// </summary>
+ internal static string LogDeleteForbid {
+ get {
+ return ResourceManager.GetString("LogDeleteForbid", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Attempt to delete a avatar of a non-existent user failed..
+ /// </summary>
+ internal static string LogDeleteNotExist {
+ get {
+ return ResourceManager.GetString("LogDeleteNotExist", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Succeed to delete a avatar of a user..
+ /// </summary>
+ internal static string LogDeleteSuccess {
+ get {
+ return ResourceManager.GetString("LogDeleteSuccess", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Attempt to get a avatar with If-None-Match in bad format..
+ /// </summary>
+ internal static string LogGetBadIfNoneMatch {
+ get {
+ return ResourceManager.GetString("LogGetBadIfNoneMatch", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Returned full data for a get avatar attempt..
+ /// </summary>
+ internal static string LogGetReturnData {
+ get {
+ return ResourceManager.GetString("LogGetReturnData", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Returned NotModify for a get avatar attempt..
+ /// </summary>
+ internal static string LogGetReturnNotModify {
+ get {
+ return ResourceManager.GetString("LogGetReturnNotModify", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Attempt to get a avatar of a non-existent user failed..
+ /// </summary>
+ internal static string LogGetUserNotExist {
+ get {
+ return ResourceManager.GetString("LogGetUserNotExist", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Attempt to put a avatar of other user as a non-admin failed..
+ /// </summary>
+ internal static string LogPutForbid {
+ get {
+ return ResourceManager.GetString("LogPutForbid", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Succeed to put a avatar of a user..
+ /// </summary>
+ internal static string LogPutSuccess {
+ get {
+ return ResourceManager.GetString("LogPutSuccess", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Attempt to put a avatar of a bad format failed..
+ /// </summary>
+ internal static string LogPutUserBadFormat {
+ get {
+ return ResourceManager.GetString("LogPutUserBadFormat", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Attempt to put a avatar of a non-existent user failed..
+ /// </summary>
+ internal static string LogPutUserNotExist {
+ get {
+ return ResourceManager.GetString("LogPutUserNotExist", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/Timeline/Resources/Controllers/UserAvatarController.en.resx b/Timeline/Resources/Controllers/UserAvatarController.en.resx
new file mode 100644
index 00000000..cf92ae6d
--- /dev/null
+++ b/Timeline/Resources/Controllers/UserAvatarController.en.resx
@@ -0,0 +1,144 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="ErrorDeleteForbid" xml:space="preserve">
+ <value>Normal user can't delete other's avatar.</value>
+ </data>
+ <data name="ErrorDeleteUserNotExist" xml:space="preserve">
+ <value>User does not exist.</value>
+ </data>
+ <data name="ErrorGetUserNotExist" xml:space="preserve">
+ <value>User does not exist.</value>
+ </data>
+ <data name="ErrorPutBadFormatBadSize" xml:space="preserve">
+ <value>Image is not a square.</value>
+ </data>
+ <data name="ErrorPutBadFormatCantDecode" xml:space="preserve">
+ <value>Decoding image failed.</value>
+ </data>
+ <data name="ErrorPutBadFormatUnmatchedFormat" xml:space="preserve">
+ <value>Image format is not the one in header.</value>
+ </data>
+ <data name="ErrorPutForbid" xml:space="preserve">
+ <value>Normal user can't change other's avatar.</value>
+ </data>
+ <data name="ErrorPutUserNotExist" xml:space="preserve">
+ <value>User does not exist.</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/Timeline/Resources/Controllers/UserAvatarController.resx b/Timeline/Resources/Controllers/UserAvatarController.resx
new file mode 100644
index 00000000..58860c83
--- /dev/null
+++ b/Timeline/Resources/Controllers/UserAvatarController.resx
@@ -0,0 +1,156 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="ExceptionUnknownAvatarFormatError" xml:space="preserve">
+ <value>Unknown AvatarDataException.ErrorReason value.</value>
+ </data>
+ <data name="LogDeleteForbid" xml:space="preserve">
+ <value>Attempt to delete a avatar of other user as a non-admin failed.</value>
+ </data>
+ <data name="LogDeleteNotExist" xml:space="preserve">
+ <value>Attempt to delete a avatar of a non-existent user failed.</value>
+ </data>
+ <data name="LogDeleteSuccess" xml:space="preserve">
+ <value>Succeed to delete a avatar of a user.</value>
+ </data>
+ <data name="LogGetBadIfNoneMatch" xml:space="preserve">
+ <value>Attempt to get a avatar with If-None-Match in bad format.</value>
+ </data>
+ <data name="LogGetReturnData" xml:space="preserve">
+ <value>Returned full data for a get avatar attempt.</value>
+ </data>
+ <data name="LogGetReturnNotModify" xml:space="preserve">
+ <value>Returned NotModify for a get avatar attempt.</value>
+ </data>
+ <data name="LogGetUserNotExist" xml:space="preserve">
+ <value>Attempt to get a avatar of a non-existent user failed.</value>
+ </data>
+ <data name="LogPutForbid" xml:space="preserve">
+ <value>Attempt to put a avatar of other user as a non-admin failed.</value>
+ </data>
+ <data name="LogPutSuccess" xml:space="preserve">
+ <value>Succeed to put a avatar of a user.</value>
+ </data>
+ <data name="LogPutUserBadFormat" xml:space="preserve">
+ <value>Attempt to put a avatar of a bad format failed.</value>
+ </data>
+ <data name="LogPutUserNotExist" xml:space="preserve">
+ <value>Attempt to put a avatar of a non-existent user failed.</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/Timeline/Resources/Controllers/UserAvatarController.zh.resx b/Timeline/Resources/Controllers/UserAvatarController.zh.resx
new file mode 100644
index 00000000..94de1606
--- /dev/null
+++ b/Timeline/Resources/Controllers/UserAvatarController.zh.resx
@@ -0,0 +1,144 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="ErrorDeleteForbid" xml:space="preserve">
+ <value>普通用户不能删除其他用户的头像。</value>
+ </data>
+ <data name="ErrorDeleteUserNotExist" xml:space="preserve">
+ <value>用户不存在。</value>
+ </data>
+ <data name="ErrorGetUserNotExist" xml:space="preserve">
+ <value>用户不存在。</value>
+ </data>
+ <data name="ErrorPutBadFormatBadSize" xml:space="preserve">
+ <value>图片不是正方形。</value>
+ </data>
+ <data name="ErrorPutBadFormatCantDecode" xml:space="preserve">
+ <value>解码图片失败。</value>
+ </data>
+ <data name="ErrorPutBadFormatUnmatchedFormat" xml:space="preserve">
+ <value>图片格式与请求头中指示的不一样。</value>
+ </data>
+ <data name="ErrorPutForbid" xml:space="preserve">
+ <value>普通用户不能修改其他用户的头像。</value>
+ </data>
+ <data name="ErrorPutUserNotExist" xml:space="preserve">
+ <value>用户不存在。</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/Timeline/Resources/Models/Http/Common.en.resx b/Timeline/Resources/Models/Http/Common.en.resx
index 40d44191..10407d76 100644
--- a/Timeline/Resources/Models/Http/Common.en.resx
+++ b/Timeline/Resources/Models/Http/Common.en.resx
@@ -117,16 +117,37 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
- <data name="ResponseDeleteDelete" xml:space="preserve">
+ <data name="ContentTooBig" xml:space="preserve">
+ <value>Body is too big. It can't be bigger than {0}.</value>
+ </data>
+ <data name="ContentUnmatchedLengthBigger" xml:space="preserve">
+ <value>Actual body length is bigger than it in header.</value>
+ </data>
+ <data name="ContentUnmatchedLengthSmaller" xml:space="preserve">
+ <value>Actual body length is smaller than it in header.</value>
+ </data>
+ <data name="DeleteDelete" xml:space="preserve">
<value>An existent item is deleted.</value>
</data>
- <data name="ResponseDeleteNotExist" xml:space="preserve">
+ <data name="DeleteNotExist" xml:space="preserve">
<value>The item does not exist, so nothing is changed.</value>
</data>
- <data name="ResponsePutCreate" xml:space="preserve">
+ <data name="HeaderBadIfNonMatch" xml:space="preserve">
+ <value>Header If-Non-Match is of bad format.</value>
+ </data>
+ <data name="HeaderMissingContentLength" xml:space="preserve">
+ <value>Header Content-Length is missing or of bad format.</value>
+ </data>
+ <data name="HeaderMissingContentType" xml:space="preserve">
+ <value>Header Content-Type is required.</value>
+ </data>
+ <data name="HeaderZeroContentLength" xml:space="preserve">
+ <value>Header Content-Length must not be 0.</value>
+ </data>
+ <data name="PutCreate" xml:space="preserve">
<value>A new item is created.</value>
</data>
- <data name="ResponsePutModify" xml:space="preserve">
+ <data name="PutModify" xml:space="preserve">
<value>An existent item is modified.</value>
</data>
</root> \ No newline at end of file
diff --git a/Timeline/Resources/Models/Http/Common.zh.resx b/Timeline/Resources/Models/Http/Common.zh.resx
index b6d955d9..528dc7ab 100644
--- a/Timeline/Resources/Models/Http/Common.zh.resx
+++ b/Timeline/Resources/Models/Http/Common.zh.resx
@@ -117,16 +117,37 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
- <data name="ResponseDeleteDelete" xml:space="preserve">
+ <data name="ContentTooBig" xml:space="preserve">
+ <value>请求体太大。它不能超过{0}.</value>
+ </data>
+ <data name="ContentUnmatchedLengthBigger" xml:space="preserve">
+ <value>实际的请求体长度比头中指示的大。</value>
+ </data>
+ <data name="ContentUnmatchedLengthSmaller" xml:space="preserve">
+ <value>实际的请求体长度比头中指示的小。</value>
+ </data>
+ <data name="DeleteDelete" xml:space="preserve">
<value>删除了一个项目。</value>
</data>
- <data name="ResponseDeleteNotExist" xml:space="preserve">
+ <data name="DeleteNotExist" xml:space="preserve">
<value>要删除的项目不存在,什么都没有修改。</value>
</data>
- <data name="ResponsePutCreate" xml:space="preserve">
+ <data name="HeaderBadIfNonMatch" xml:space="preserve">
+ <value>头If-Non-Match格式不对。</value>
+ </data>
+ <data name="HeaderMissingContentLength" xml:space="preserve">
+ <value>头Content-Length缺失或者格式不对。</value>
+ </data>
+ <data name="HeaderMissingContentType" xml:space="preserve">
+ <value>缺少必需的头Content-Type。</value>
+ </data>
+ <data name="HeaderZeroContentLength" xml:space="preserve">
+ <value>头Content-Length不能为0。</value>
+ </data>
+ <data name="PutCreate" xml:space="preserve">
<value>创建了一个新项目。</value>
</data>
- <data name="ResponsePutModify" xml:space="preserve">
+ <data name="PutModify" xml:space="preserve">
<value>修改了一个已存在的项目。</value>
</data>
</root> \ No newline at end of file
diff --git a/Timeline/Resources/Services/Exception.Designer.cs b/Timeline/Resources/Services/Exception.Designer.cs
index 24f6b8e6..ddf60f45 100644
--- a/Timeline/Resources/Services/Exception.Designer.cs
+++ b/Timeline/Resources/Services/Exception.Designer.cs
@@ -61,6 +61,51 @@ namespace Timeline.Resources.Services {
}
/// <summary>
+ /// Looks up a localized string similar to Avartar is of bad format because {0}..
+ /// </summary>
+ internal static string AvatarFormatException {
+ get {
+ return ResourceManager.GetString("AvatarFormatException", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to image is not a square, aka, width is not equal to height.
+ /// </summary>
+ internal static string AvatarFormatExceptionBadSize {
+ get {
+ return ResourceManager.GetString("AvatarFormatExceptionBadSize", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to failed to decode image, see inner exception.
+ /// </summary>
+ internal static string AvatarFormatExceptionCantDecode {
+ get {
+ return ResourceManager.GetString("AvatarFormatExceptionCantDecode", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to unknown error.
+ /// </summary>
+ internal static string AvatarFormatExceptionUnknownError {
+ get {
+ return ResourceManager.GetString("AvatarFormatExceptionUnknownError", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to image&apos;s actual mime type is not the specified one.
+ /// </summary>
+ internal static string AvatarFormatExceptionUnmatchedFormat {
+ get {
+ return ResourceManager.GetString("AvatarFormatExceptionUnmatchedFormat", resourceCulture);
+ }
+ }
+
+ /// <summary>
/// Looks up a localized string similar to The password is wrong..
/// </summary>
internal static string BadPasswordException {
diff --git a/Timeline/Resources/Services/Exception.resx b/Timeline/Resources/Services/Exception.resx
index 408c45a1..12bf9afb 100644
--- a/Timeline/Resources/Services/Exception.resx
+++ b/Timeline/Resources/Services/Exception.resx
@@ -117,6 +117,21 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
+ <data name="AvatarFormatException" xml:space="preserve">
+ <value>Avartar is of bad format because {0}.</value>
+ </data>
+ <data name="AvatarFormatExceptionBadSize" xml:space="preserve">
+ <value>image is not a square, aka, width is not equal to height</value>
+ </data>
+ <data name="AvatarFormatExceptionCantDecode" xml:space="preserve">
+ <value>failed to decode image, see inner exception</value>
+ </data>
+ <data name="AvatarFormatExceptionUnknownError" xml:space="preserve">
+ <value>unknown error</value>
+ </data>
+ <data name="AvatarFormatExceptionUnmatchedFormat" xml:space="preserve">
+ <value>image's actual mime type is not the specified one</value>
+ </data>
<data name="BadPasswordException" xml:space="preserve">
<value>The password is wrong.</value>
</data>
diff --git a/Timeline/Resources/Services/UserAvatarService.Designer.cs b/Timeline/Resources/Services/UserAvatarService.Designer.cs
new file mode 100644
index 00000000..cabc9ede
--- /dev/null
+++ b/Timeline/Resources/Services/UserAvatarService.Designer.cs
@@ -0,0 +1,108 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Timeline.Resources.Services {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class UserAvatarService {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal UserAvatarService() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Services.UserAvatarService", typeof(UserAvatarService).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Data of avatar is null..
+ /// </summary>
+ internal static string ArgumentAvatarDataNull {
+ get {
+ return ResourceManager.GetString("ArgumentAvatarDataNull", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Type of avatar is null..
+ /// </summary>
+ internal static string ArgumentAvatarTypeNull {
+ get {
+ return ResourceManager.GetString("ArgumentAvatarTypeNull", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Database corupted! One of type and data of a avatar is null but the other is not..
+ /// </summary>
+ internal static string DatabaseCorruptedDataAndTypeNotSame {
+ get {
+ return ResourceManager.GetString("DatabaseCorruptedDataAndTypeNotSame", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Created an entry in user_avatars..
+ /// </summary>
+ internal static string LogCreateEntity {
+ get {
+ return ResourceManager.GetString("LogCreateEntity", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Updated an entry in user_avatars..
+ /// </summary>
+ internal static string LogUpdateEntity {
+ get {
+ return ResourceManager.GetString("LogUpdateEntity", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/Timeline/Resources/Services/UserAvatarService.resx b/Timeline/Resources/Services/UserAvatarService.resx
new file mode 100644
index 00000000..ab6389ff
--- /dev/null
+++ b/Timeline/Resources/Services/UserAvatarService.resx
@@ -0,0 +1,135 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="ArgumentAvatarDataNull" xml:space="preserve">
+ <value>Data of avatar is null.</value>
+ </data>
+ <data name="ArgumentAvatarTypeNull" xml:space="preserve">
+ <value>Type of avatar is null.</value>
+ </data>
+ <data name="DatabaseCorruptedDataAndTypeNotSame" xml:space="preserve">
+ <value>Database corupted! One of type and data of a avatar is null but the other is not.</value>
+ </data>
+ <data name="LogCreateEntity" xml:space="preserve">
+ <value>Created an entry in user_avatars.</value>
+ </data>
+ <data name="LogUpdateEntity" xml:space="preserve">
+ <value>Updated an entry in user_avatars.</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/Timeline/Services/AvatarFormatException.cs b/Timeline/Services/AvatarFormatException.cs
new file mode 100644
index 00000000..788eabb2
--- /dev/null
+++ b/Timeline/Services/AvatarFormatException.cs
@@ -0,0 +1,51 @@
+using System;
+using System.Globalization;
+
+namespace Timeline.Services
+{
+ /// <summary>
+ /// Thrown when avatar is of bad format.
+ /// </summary>
+ [Serializable]
+ public class AvatarFormatException : Exception
+ {
+ public enum ErrorReason
+ {
+ /// <summary>
+ /// Decoding image failed.
+ /// </summary>
+ CantDecode,
+ /// <summary>
+ /// Decoding succeeded but the real type is not the specified type.
+ /// </summary>
+ UnmatchedFormat,
+ /// <summary>
+ /// Image is not a square.
+ /// </summary>
+ BadSize
+ }
+
+ public AvatarFormatException() : base(MakeMessage(null)) { }
+ public AvatarFormatException(string message) : base(message) { }
+ public AvatarFormatException(string message, Exception inner) : base(message, inner) { }
+
+ public AvatarFormatException(Avatar avatar, ErrorReason error) : base(MakeMessage(error)) { Avatar = avatar; Error = error; }
+ public AvatarFormatException(Avatar avatar, ErrorReason error, Exception inner) : base(MakeMessage(error), inner) { Avatar = avatar; Error = error; }
+
+ protected AvatarFormatException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+
+ private static string MakeMessage(ErrorReason? reason) =>
+ string.Format(CultureInfo.InvariantCulture, Resources.Services.Exception.AvatarFormatException, reason switch
+ {
+ ErrorReason.CantDecode => Resources.Services.Exception.AvatarFormatExceptionCantDecode,
+ ErrorReason.UnmatchedFormat => Resources.Services.Exception.AvatarFormatExceptionUnmatchedFormat,
+ ErrorReason.BadSize => Resources.Services.Exception.AvatarFormatExceptionBadSize,
+ _ => Resources.Services.Exception.AvatarFormatExceptionUnknownError
+ });
+
+ public ErrorReason? Error { get; set; }
+ public Avatar? Avatar { get; set; }
+ }
+}
diff --git a/Timeline/Services/DatabaseExtensions.cs b/Timeline/Services/DatabaseExtensions.cs
index a37cf05b..62b22f00 100644
--- a/Timeline/Services/DatabaseExtensions.cs
+++ b/Timeline/Services/DatabaseExtensions.cs
@@ -4,22 +4,27 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Timeline.Entities;
+using Timeline.Models.Validation;
namespace Timeline.Services
{
- public static class DatabaseExtensions
+ internal static class DatabaseExtensions
{
/// <summary>
/// Check the existence and get the id of the user.
/// </summary>
/// <param name="username">The username of the user.</param>
/// <returns>The user id.</returns>
- /// <exception cref="ArgumentException">Thrown if <paramref name="username"/> is null or empty.</exception>
+ /// <exception cref="ArgumentNullException">Thrown if <paramref name="username"/> is null.</exception>
+ /// <exception cref="UsernameBadFormatException">Thrown if <paramref name="username"/> is of bad format.</exception>
/// <exception cref="UserNotExistException">Thrown if user does not exist.</exception>
- public static async Task<long> CheckAndGetUser(DbSet<User> userDbSet, string username)
+ internal static async Task<long> CheckAndGetUser(DbSet<User> userDbSet, UsernameValidator validator, string username)
{
- if (string.IsNullOrEmpty(username))
- throw new ArgumentException("Username is null or empty.", nameof(username));
+ if (username == null)
+ throw new ArgumentNullException(nameof(username));
+ var (result, messageGenerator) = validator.Validate(username);
+ if (!result)
+ throw new UsernameBadFormatException(username, messageGenerator(null));
var userId = await userDbSet.Where(u => u.Name == username).Select(u => u.Id).SingleOrDefaultAsync();
if (userId == 0)
diff --git a/Timeline/Services/ETagGenerator.cs b/Timeline/Services/ETagGenerator.cs
index e2abebdc..e518f01f 100644
--- a/Timeline/Services/ETagGenerator.cs
+++ b/Timeline/Services/ETagGenerator.cs
@@ -5,13 +5,20 @@ namespace Timeline.Services
{
public interface IETagGenerator
{
+ /// <summary>
+ /// Generate a etag for given source.
+ /// </summary>
+ /// <param name="source">The source data.</param>
+ /// <returns>The generated etag.</returns>
+ /// <exception cref="ArgumentNullException">Thrown if <paramref name="source"/> is null.</exception>
string Generate(byte[] source);
}
- public class ETagGenerator : IETagGenerator, IDisposable
+ public sealed class ETagGenerator : IETagGenerator, IDisposable
{
private readonly SHA1 _sha1;
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Security", "CA5350:Do Not Use Weak Cryptographic Algorithms", Justification = "Sha1 is enough ??? I don't know.")]
public ETagGenerator()
{
_sha1 = SHA1.Create();
@@ -19,15 +26,19 @@ namespace Timeline.Services
public string Generate(byte[] source)
{
- if (source == null || source.Length == 0)
- throw new ArgumentException("Source is null or empty.", nameof(source));
+ if (source == null)
+ throw new ArgumentNullException(nameof(source));
return Convert.ToBase64String(_sha1.ComputeHash(source));
}
+ private bool _disposed = false; // To detect redundant calls
+
public void Dispose()
{
+ if (_disposed) return;
_sha1.Dispose();
+ _disposed = true;
}
}
}
diff --git a/Timeline/Services/UserAvatarService.cs b/Timeline/Services/UserAvatarService.cs
index ecec5a31..4c65a0fa 100644
--- a/Timeline/Services/UserAvatarService.cs
+++ b/Timeline/Services/UserAvatarService.cs
@@ -10,54 +10,25 @@ using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Timeline.Entities;
+using Timeline.Helpers;
+using Timeline.Models.Validation;
namespace Timeline.Services
{
public class Avatar
{
- public string Type { get; set; }
- public byte[] Data { get; set; }
+ 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; }
+ public Avatar Avatar { get; set; } = default!;
public DateTime LastModified { get; set; }
}
/// <summary>
- /// Thrown when avatar is of bad format.
- /// </summary>
- [Serializable]
- public class AvatarDataException : Exception
- {
- public enum ErrorReason
- {
- /// <summary>
- /// Decoding image failed.
- /// </summary>
- CantDecode,
- /// <summary>
- /// Decoding succeeded but the real type is not the specified type.
- /// </summary>
- UnmatchedFormat,
- /// <summary>
- /// Image is not a square.
- /// </summary>
- 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; }
- }
-
- /// <summary>
/// Provider for default user avatar.
/// </summary>
/// <remarks>
@@ -83,7 +54,7 @@ namespace Timeline.Services
/// Validate a avatar's format and size info.
/// </summary>
/// <param name="avatar">The avatar to validate.</param>
- /// <exception cref="AvatarDataException">Thrown when validation failed.</exception>
+ /// <exception cref="AvatarFormatException">Thrown when validation failed.</exception>
Task Validate(Avatar avatar);
}
@@ -94,16 +65,18 @@ namespace Timeline.Services
/// </summary>
/// <param name="username">The username of the user to get avatar etag of.</param>
/// <returns>The etag.</returns>
- /// <exception cref="ArgumentException">Thrown if <paramref name="username"/> is null or empty.</exception>
+ /// <exception cref="ArgumentNullException">Thrown if <paramref name="username"/> is null.</exception>
+ /// <exception cref="UsernameBadFormatException">Thrown if the <paramref name="username"/> is of bad format.</exception>
/// <exception cref="UserNotExistException">Thrown if the user does not exist.</exception>
Task<string> GetAvatarETag(string username);
/// <summary>
- /// Get avatar of a user. If the user has no avatar, a default one is returned.
+ /// Get avatar of a user. If the user has no avatar set, a default one is returned.
/// </summary>
/// <param name="username">The username of the user to get avatar of.</param>
/// <returns>The avatar info.</returns>
- /// <exception cref="ArgumentException">Thrown if <paramref name="username"/> is null or empty.</exception>
+ /// <exception cref="ArgumentNullException">Thrown if <paramref name="username"/> is null.</exception>
+ /// <exception cref="UsernameBadFormatException">Thrown if the <paramref name="username"/> is of bad format.</exception>
/// <exception cref="UserNotExistException">Thrown if the user does not exist.</exception>
Task<AvatarInfo> GetAvatar(string username);
@@ -112,38 +85,41 @@ namespace Timeline.Services
/// </summary>
/// <param name="username">The username of the user to set avatar for.</param>
/// <param name="avatar">The avatar. Can be null to delete the saved avatar.</param>
- /// <exception cref="ArgumentException">Throw if <paramref name="username"/> is null or empty.
- /// Or thrown if <paramref name="avatar"/> is not null but <see cref="Avatar.Type"/> is null or empty or <see cref="Avatar.Data"/> is null.</exception>
+ /// <exception cref="ArgumentNullException">Throw if <paramref name="username"/> is null.</exception>
+ /// <exception cref="ArgumentException">Thrown if any field in <paramref name="avatar"/> is null when <paramref name="avatar"/> is not null.</exception>
+ /// <exception cref="UsernameBadFormatException">Thrown if the <paramref name="username"/> is of bad format.</exception>
/// <exception cref="UserNotExistException">Thrown if the user does not exist.</exception>
- /// <exception cref="AvatarDataException">Thrown if avatar is of bad format.</exception>
- Task SetAvatar(string username, Avatar avatar);
+ /// <exception cref="AvatarFormatException">Thrown if avatar is of bad format.</exception>
+ Task SetAvatar(string username, Avatar? avatar);
}
+ // TODO! : Make this configurable.
public class DefaultUserAvatarProvider : IDefaultUserAvatarProvider
{
- private readonly IWebHostEnvironment _environment;
-
private readonly IETagGenerator _eTagGenerator;
- private byte[] _cacheData;
+ private readonly string _avatarPath;
+
+ private byte[] _cacheData = default!;
private DateTime _cacheLastModified;
- private string _cacheETag;
+ private string _cacheETag = default!;
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "DI.")]
public DefaultUserAvatarProvider(IWebHostEnvironment environment, IETagGenerator eTagGenerator)
{
- _environment = environment;
+ _avatarPath = Path.Combine(environment.ContentRootPath, "default-avatar.png");
_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);
+ var path = _avatarPath;
+ if (_cacheData == null || File.GetLastWriteTime(path) > _cacheLastModified)
+ {
+ _cacheData = await File.ReadAllBytesAsync(path);
+ _cacheLastModified = File.GetLastWriteTime(path);
+ _cacheETag = _eTagGenerator.Generate(_cacheData);
+ }
}
public async Task<string> GetDefaultAvatarETag()
@@ -175,17 +151,15 @@ namespace Timeline.Services
{
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.");
- }
+ using var image = Image.Load(avatar.Data, out IImageFormat format);
+ if (!format.MimeTypes.Contains(avatar.Type))
+ throw new AvatarFormatException(avatar, AvatarFormatException.ErrorReason.UnmatchedFormat);
+ if (image.Width != image.Height)
+ throw new AvatarFormatException(avatar, AvatarFormatException.ErrorReason.BadSize);
}
catch (UnknownImageFormatException e)
{
- throw new AvatarDataException(avatar, AvatarDataException.ErrorReason.CantDecode, "Failed to decode image. See inner exception.", e);
+ throw new AvatarFormatException(avatar, AvatarFormatException.ErrorReason.CantDecode, e);
}
});
}
@@ -203,6 +177,8 @@ namespace Timeline.Services
private readonly IETagGenerator _eTagGenerator;
+ private readonly UsernameValidator _usernameValidator;
+
public UserAvatarService(
ILogger<UserAvatarService> logger,
DatabaseContext database,
@@ -215,13 +191,14 @@ namespace Timeline.Services
_defaultUserAvatarProvider = defaultUserAvatarProvider;
_avatarValidator = avatarValidator;
_eTagGenerator = eTagGenerator;
+ _usernameValidator = new UsernameValidator();
}
public async Task<string> GetAvatarETag(string username)
{
- var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username);
+ var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, _usernameValidator, username);
- var eTag = (await _database.UserAvatars.Where(a => a.UserId == userId).Select(a => new { a.ETag }).SingleAsync()).ETag;
+ var eTag = (await _database.UserAvatars.Where(a => a.UserId == userId).Select(a => new { a.ETag }).SingleOrDefaultAsync())?.ETag;
if (eTag == null)
return await _defaultUserAvatarProvider.GetDefaultAvatarETag();
else
@@ -230,54 +207,57 @@ namespace Timeline.Services
public async Task<AvatarInfo> GetAvatar(string username)
{
- var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username);
+ var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, _usernameValidator, username);
- var avatar = await _database.UserAvatars.Where(a => a.UserId == userId).Select(a => new { a.Type, a.Data, a.LastModified }).SingleAsync();
+ var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == userId).Select(a => new { a.Type, a.Data, a.LastModified }).SingleOrDefaultAsync();
- if ((avatar.Type == null) != (avatar.Data == null))
+ if (avatarEntity != null)
{
- _logger.LogCritical("Database corupted! One of type and data of a avatar is null but the other is not.");
- throw new DatabaseCorruptedException();
- }
+ if (!LanguageHelper.AreSame(avatarEntity.Data == null, avatarEntity.Type == null))
+ {
+ var message = Resources.Services.UserAvatarService.DatabaseCorruptedDataAndTypeNotSame;
+ _logger.LogCritical(message);
+ throw new DatabaseCorruptedException(message);
+ }
- 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
+ if (avatarEntity.Data != null)
{
- Avatar = new Avatar
+ return new AvatarInfo
{
- Type = avatar.Type,
- Data = avatar.Data
- },
- LastModified = avatar.LastModified
- };
+ Avatar = new Avatar
+ {
+ Type = avatarEntity.Type!,
+ Data = avatarEntity.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(string username, Avatar avatar)
+ 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));
+ throw new ArgumentException(Resources.Services.UserAvatarService.ArgumentAvatarDataNull, nameof(avatar));
+ if (avatar.Type == null)
+ throw new ArgumentException(Resources.Services.UserAvatarService.ArgumentAvatarTypeNull, nameof(avatar));
}
- var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username);
-
- var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == userId).SingleAsync();
+ var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, _usernameValidator, username);
+ var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == userId).SingleOrDefaultAsync();
if (avatar == null)
{
- if (avatarEntity.Data == null)
+ if (avatarEntity == null || avatarEntity.Data == null)
+ {
return;
+ }
else
{
avatarEntity.Data = null;
@@ -285,18 +265,29 @@ namespace Timeline.Services
avatarEntity.ETag = null;
avatarEntity.LastModified = DateTime.Now;
await _database.SaveChangesAsync();
- _logger.LogInformation("Updated an entry in user_avatars.");
+ _logger.LogInformation(Resources.Services.UserAvatarService.LogUpdateEntity);
}
}
else
{
await _avatarValidator.Validate(avatar);
- avatarEntity.Type = avatar.Type;
+ var create = avatarEntity == null;
+ if (create)
+ {
+ avatarEntity = new UserAvatar();
+ }
+ avatarEntity!.Type = avatar.Type;
avatarEntity.Data = avatar.Data;
avatarEntity.ETag = _eTagGenerator.Generate(avatar.Data);
avatarEntity.LastModified = DateTime.Now;
+ if (create)
+ {
+ _database.UserAvatars.Add(avatarEntity);
+ }
await _database.SaveChangesAsync();
- _logger.LogInformation("Updated an entry in user_avatars.");
+ _logger.LogInformation(create ?
+ Resources.Services.UserAvatarService.LogCreateEntity
+ : Resources.Services.UserAvatarService.LogUpdateEntity);
}
}
}
@@ -308,7 +299,7 @@ namespace Timeline.Services
services.TryAddTransient<IETagGenerator, ETagGenerator>();
services.AddScoped<IUserAvatarService, UserAvatarService>();
services.AddSingleton<IDefaultUserAvatarProvider, DefaultUserAvatarProvider>();
- services.AddSingleton<IUserAvatarValidator, UserAvatarValidator>();
+ services.AddTransient<IUserAvatarValidator, UserAvatarValidator>();
}
}
}
diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs
index d706d05e..f1317856 100644
--- a/Timeline/Services/UserService.cs
+++ b/Timeline/Services/UserService.cs
@@ -272,7 +272,7 @@ namespace Timeline.Services
Name = username,
EncryptedPassword = _passwordService.HashPassword(password),
RoleString = UserRoleConvert.ToString(administrator),
- Avatar = UserAvatar.Create(DateTime.Now)
+ Avatar = null
};
await _databaseContext.AddAsync(newUser);
await _databaseContext.SaveChangesAsync();
diff --git a/Timeline/Timeline.csproj b/Timeline/Timeline.csproj
index 0ba34471..519a802d 100644
--- a/Timeline/Timeline.csproj
+++ b/Timeline/Timeline.csproj
@@ -49,6 +49,11 @@
<AutoGen>True</AutoGen>
<DependentUpon>TokenController.resx</DependentUpon>
</Compile>
+ <Compile Update="Resources\Controllers\UserAvatarController.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>UserAvatarController.resx</DependentUpon>
+ </Compile>
<Compile Update="Resources\Controllers\UserController.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
@@ -69,6 +74,11 @@
<AutoGen>True</AutoGen>
<DependentUpon>Exception.resx</DependentUpon>
</Compile>
+ <Compile Update="Resources\Services\UserAvatarService.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>UserAvatarService.resx</DependentUpon>
+ </Compile>
<Compile Update="Resources\Services\UserService.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
@@ -97,6 +107,10 @@
<EmbeddedResource Update="Resources\Controllers\TokenController.en.resx">
<Generator></Generator>
</EmbeddedResource>
+ <EmbeddedResource Update="Resources\Controllers\UserAvatarController.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>UserAvatarController.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
<EmbeddedResource Update="Resources\Controllers\UserController.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>UserController.Designer.cs</LastGenOutput>
@@ -116,6 +130,10 @@
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Exception.Designer.cs</LastGenOutput>
</EmbeddedResource>
+ <EmbeddedResource Update="Resources\Services\UserAvatarService.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>UserAvatarService.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
<EmbeddedResource Update="Resources\Services\UserService.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>UserService.Designer.cs</LastGenOutput>