aboutsummaryrefslogtreecommitdiff
path: root/BackEnd
diff options
context:
space:
mode:
Diffstat (limited to 'BackEnd')
-rw-r--r--BackEnd/Timeline/Controllers/UserAvatarController.cs2
-rw-r--r--BackEnd/Timeline/Services/BasicServicesServiceCollectionExtensions.cs18
-rw-r--r--BackEnd/Timeline/Services/Data/DataManager.cs93
-rw-r--r--BackEnd/Timeline/Services/Data/DataManagerExtensions.cs27
-rw-r--r--BackEnd/Timeline/Services/Data/DataServicesServiceCollectionExtensions.cs15
-rw-r--r--BackEnd/Timeline/Services/Data/ETagGenerator.cs16
-rw-r--r--BackEnd/Timeline/Services/Data/IDataManager.cs49
-rw-r--r--BackEnd/Timeline/Services/Data/IETagGenerator.cs19
-rw-r--r--BackEnd/Timeline/Services/Data/Resource.Designer.cs117
-rw-r--r--BackEnd/Timeline/Services/Data/Resource.resx138
-rw-r--r--BackEnd/Timeline/Services/DatabaseManagement/DatabaseBackupService.cs11
-rw-r--r--BackEnd/Timeline/Services/DatabaseManagement/DatabaseCustomMigrator.cs17
-rw-r--r--BackEnd/Timeline/Services/DatabaseManagement/IDatabaseBackupService.cs10
-rw-r--r--BackEnd/Timeline/Services/DatabaseManagement/IDatabaseCustomMigration.cs9
-rw-r--r--BackEnd/Timeline/Services/DatabaseManagement/IDatabaseCustomMigrator.cs10
-rw-r--r--BackEnd/Timeline/Services/DatabaseManagement/MigationServiceCollectionExtensions.cs9
-rw-r--r--BackEnd/Timeline/Services/DatabaseManagement/Resource.Designer.cs (renamed from BackEnd/Timeline/Resources/Services/DataManager.Designer.cs)50
-rw-r--r--BackEnd/Timeline/Services/DatabaseManagement/Resource.resx (renamed from BackEnd/Timeline/Resources/Services/DataManager.resx)16
-rw-r--r--BackEnd/Timeline/Services/DatabaseManagement/TimelinePostContentToDataMigration.cs4
-rw-r--r--BackEnd/Timeline/Services/Imaging/IImageService.cs32
-rw-r--r--BackEnd/Timeline/Services/Imaging/ImageException.cs7
-rw-r--r--BackEnd/Timeline/Services/Imaging/ImageService.cs54
-rw-r--r--BackEnd/Timeline/Services/Imaging/ImageServicesServiceCollectionExtensions.cs14
-rw-r--r--BackEnd/Timeline/Services/Imaging/ImageValidator.cs53
-rw-r--r--BackEnd/Timeline/Services/Timeline/TimelinePostService.cs12
-rw-r--r--BackEnd/Timeline/Services/User/UserAvatarService.cs14
-rw-r--r--BackEnd/Timeline/Startup.cs14
-rw-r--r--BackEnd/Timeline/Timeline.csproj27
28 files changed, 657 insertions, 200 deletions
diff --git a/BackEnd/Timeline/Controllers/UserAvatarController.cs b/BackEnd/Timeline/Controllers/UserAvatarController.cs
index 158c342e..d0998fa7 100644
--- a/BackEnd/Timeline/Controllers/UserAvatarController.cs
+++ b/BackEnd/Timeline/Controllers/UserAvatarController.cs
@@ -117,7 +117,7 @@ namespace Timeline.Controllers
{
ImageException.ErrorReason.CantDecode => ErrorResponse.UserAvatar.BadFormat_CantDecode(),
ImageException.ErrorReason.UnmatchedFormat => ErrorResponse.UserAvatar.BadFormat_UnmatchedFormat(),
- ImageException.ErrorReason.NotSquare => ErrorResponse.UserAvatar.BadFormat_BadSize(),
+ ImageException.ErrorReason.BadSize => ErrorResponse.UserAvatar.BadFormat_BadSize(),
_ =>
throw new Exception(ExceptionUnknownAvatarFormatError)
});
diff --git a/BackEnd/Timeline/Services/BasicServicesServiceCollectionExtensions.cs b/BackEnd/Timeline/Services/BasicServicesServiceCollectionExtensions.cs
new file mode 100644
index 00000000..6465fb9f
--- /dev/null
+++ b/BackEnd/Timeline/Services/BasicServicesServiceCollectionExtensions.cs
@@ -0,0 +1,18 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Timeline.Services
+{
+ public static class BasicServicesServiceCollectionExtensions
+ {
+ public static IServiceCollection AddBasicServices(this IServiceCollection services)
+ {
+ services.TryAddSingleton<IPathProvider, PathProvider>();
+ services.TryAddTransient<IClock, Clock>();
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Services/Data/DataManager.cs b/BackEnd/Timeline/Services/Data/DataManager.cs
index d9a4491d..6cf3225f 100644
--- a/BackEnd/Timeline/Services/Data/DataManager.cs
+++ b/BackEnd/Timeline/Services/Data/DataManager.cs
@@ -1,73 +1,40 @@
using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
using System;
using System.Linq;
+using System.Threading;
using System.Threading.Tasks;
using Timeline.Entities;
namespace Timeline.Services.Data
{
- /// <summary>
- /// A data manager controlling data.
- /// </summary>
- /// <remarks>
- /// Identical data will be saved as one copy and return the same tag.
- /// Every data has a ref count. When data is retained, ref count increase.
- /// When data is freed, ref count decease. If ref count is decreased
- /// to 0, the data entry will be destroyed and no longer occupy space.
- /// </remarks>
- public interface IDataManager
- {
- /// <summary>
- /// Saves the data to a new entry if it does not exist,
- /// increases its ref count and returns a tag to the entry.
- /// </summary>
- /// <param name="data">The data. Can't be null.</param>
- /// <returns>The tag of the created entry.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="data"/> is null.</exception>
- public Task<string> RetainEntry(byte[] data);
-
- /// <summary>
- /// Decrease the the ref count of the entry.
- /// Remove it if ref count is zero.
- /// </summary>
- /// <param name="tag">The tag of the entry.</param>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="tag"/> is null.</exception>
- /// <remarks>
- /// It's no-op if entry with tag does not exist.
- /// </remarks>
- public Task FreeEntry(string tag);
-
- /// <summary>
- /// Retrieve the entry with given tag. If not exist, returns null.
- /// </summary>
- /// <param name="tag">The tag of the entry.</param>
- /// <returns>The data of the entry. If not exist, returns null.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="tag"/> is null.</exception>
- public Task<byte[]?> GetEntry(string tag);
- }
-
public class DataManager : IDataManager
{
+ private readonly ILogger<DataManager> _logger;
private readonly DatabaseContext _database;
private readonly IETagGenerator _eTagGenerator;
- public DataManager(DatabaseContext database, IETagGenerator eTagGenerator)
+ public DataManager(ILogger<DataManager> logger, DatabaseContext database, IETagGenerator eTagGenerator)
{
+ _logger = logger;
_database = database;
_eTagGenerator = eTagGenerator;
}
- public async Task<string> RetainEntry(byte[] data)
+ public async Task<string> RetainEntryAsync(byte[] data, CancellationToken cancellationToken = default)
{
if (data == null)
throw new ArgumentNullException(nameof(data));
- var tag = await _eTagGenerator.Generate(data);
+ var tag = await _eTagGenerator.GenerateETagAsync(data, cancellationToken);
+
+ var entity = await _database.Data.Where(d => d.Tag == tag).SingleOrDefaultAsync(cancellationToken);
+ bool create;
- var entity = await _database.Data.Where(d => d.Tag == tag).SingleOrDefaultAsync();
if (entity == null)
{
+ create = true;
entity = new DataEntity
{
Tag = tag,
@@ -78,42 +45,54 @@ namespace Timeline.Services.Data
}
else
{
+ create = false;
entity.Ref += 1;
}
- await _database.SaveChangesAsync();
+ await _database.SaveChangesAsync(cancellationToken);
+
+ _logger.LogInformation(create ? Resource.LogDataManagerRetainEntryCreate : Resource.LogDataManagerRetainEntryAddRefCount, tag);
return tag;
}
- public async Task FreeEntry(string tag)
+ public async Task FreeEntryAsync(string tag, CancellationToken cancellationToken = default)
{
if (tag == null)
throw new ArgumentNullException(nameof(tag));
- var entity = await _database.Data.Where(d => d.Tag == tag).SingleOrDefaultAsync();
+ var entity = await _database.Data.Where(d => d.Tag == tag).SingleOrDefaultAsync(cancellationToken);
if (entity != null)
{
+ bool remove;
+
if (entity.Ref == 1)
{
+ remove = true;
_database.Data.Remove(entity);
}
else
{
+ remove = false;
entity.Ref -= 1;
}
- await _database.SaveChangesAsync();
+ await _database.SaveChangesAsync(cancellationToken);
+ _logger.LogInformation(remove ? Resource.LogDataManagerFreeEntryRemove : Resource.LogDataManagerFreeEntryDecreaseRefCount, tag);
+ }
+ else
+ {
+ _logger.LogInformation(Resource.LogDataManagerFreeEntryNotExist, tag);
}
}
- public async Task<byte[]?> GetEntry(string tag)
+ public async Task<byte[]?> GetEntryAsync(string tag, CancellationToken cancellationToken = default)
{
if (tag == null)
throw new ArgumentNullException(nameof(tag));
- var entity = await _database.Data.Where(d => d.Tag == tag).Select(d => new { d.Data }).SingleOrDefaultAsync();
+ var entity = await _database.Data.Where(d => d.Tag == tag).Select(d => new { d.Data }).SingleOrDefaultAsync(cancellationToken);
if (entity is null)
return null;
@@ -121,18 +100,4 @@ namespace Timeline.Services.Data
return entity.Data;
}
}
-
- public static class DataManagerExtensions
- {
- /// <summary>
- /// Try to get an entry and throw <see cref="DatabaseCorruptedException"/> if not exist.
- /// </summary>
- public static async Task<byte[]> GetEntryAndCheck(this IDataManager dataManager, string tag, string notExistMessage)
- {
- var data = await dataManager.GetEntry(tag);
- if (data is null)
- throw new DatabaseCorruptedException($"Can't get data of tag {tag}. {notExistMessage}");
- return data;
- }
- }
}
diff --git a/BackEnd/Timeline/Services/Data/DataManagerExtensions.cs b/BackEnd/Timeline/Services/Data/DataManagerExtensions.cs
new file mode 100644
index 00000000..d149f3fa
--- /dev/null
+++ b/BackEnd/Timeline/Services/Data/DataManagerExtensions.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Timeline.Services.Data
+{
+ public static class DataManagerExtensions
+ {
+ /// <summary>
+ /// Try to get an entry and throw <see cref="DatabaseCorruptedException"/> if not exist.
+ /// </summary>
+ public static async Task<byte[]> GetEntryAndCheck(this IDataManager dataManager, string tag, string notExistMessage, CancellationToken cancellationToken = default)
+ {
+ if (dataManager is null)
+ throw new ArgumentNullException(nameof(dataManager));
+ if (tag is null)
+ throw new ArgumentNullException(nameof(tag));
+ if (notExistMessage is null)
+ throw new ArgumentNullException(nameof(notExistMessage));
+
+ var data = await dataManager.GetEntryAsync(tag, cancellationToken);
+ if (data is null)
+ throw new DatabaseCorruptedException(string.Format(Resource.GetEntryAndCheckNotExist, tag, notExistMessage));
+ return data;
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Services/Data/DataServicesServiceCollectionExtensions.cs b/BackEnd/Timeline/Services/Data/DataServicesServiceCollectionExtensions.cs
new file mode 100644
index 00000000..2717e63c
--- /dev/null
+++ b/BackEnd/Timeline/Services/Data/DataServicesServiceCollectionExtensions.cs
@@ -0,0 +1,15 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+
+namespace Timeline.Services.Data
+{
+ public static class DataServicesServiceCollectionExtensions
+ {
+ public static IServiceCollection AddDataServices(this IServiceCollection services)
+ {
+ services.TryAddScoped<IETagGenerator, ETagGenerator>();
+ services.TryAddScoped<IDataManager, DataManager>();
+ return services;
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Services/Data/ETagGenerator.cs b/BackEnd/Timeline/Services/Data/ETagGenerator.cs
index 847c120b..03837dc9 100644
--- a/BackEnd/Timeline/Services/Data/ETagGenerator.cs
+++ b/BackEnd/Timeline/Services/Data/ETagGenerator.cs
@@ -1,20 +1,10 @@
using System;
using System.Security.Cryptography;
+using System.Threading;
using System.Threading.Tasks;
namespace Timeline.Services.Data
{
- 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>
- Task<string> Generate(byte[] source);
- }
-
public sealed class ETagGenerator : IETagGenerator, IDisposable
{
private readonly SHA1 _sha1;
@@ -25,12 +15,12 @@ namespace Timeline.Services.Data
_sha1 = SHA1.Create();
}
- public Task<string> Generate(byte[] source)
+ public Task<string> GenerateETagAsync(byte[] source, CancellationToken cancellationToken = default)
{
if (source == null)
throw new ArgumentNullException(nameof(source));
- return Task.Run(() => Convert.ToBase64String(_sha1.ComputeHash(source)));
+ return Task.Run(() => Convert.ToBase64String(_sha1.ComputeHash(source)), cancellationToken);
}
private bool _disposed; // To detect redundant calls
diff --git a/BackEnd/Timeline/Services/Data/IDataManager.cs b/BackEnd/Timeline/Services/Data/IDataManager.cs
new file mode 100644
index 00000000..4191367e
--- /dev/null
+++ b/BackEnd/Timeline/Services/Data/IDataManager.cs
@@ -0,0 +1,49 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Timeline.Services.Data
+{
+ /// <summary>
+ /// A data manager controlling data.
+ /// </summary>
+ /// <remarks>
+ /// Identical data will be saved as one copy and return the same tag.
+ /// Every data has a ref count. When data is retained, ref count increase.
+ /// When data is freed, ref count decease. If ref count is decreased
+ /// to 0, the data entry will be destroyed and no longer occupy space.
+ /// </remarks>
+ public interface IDataManager
+ {
+ /// <summary>
+ /// Saves the data to a new entry if it does not exist,
+ /// increases its ref count and returns a tag to the entry.
+ /// </summary>
+ /// <param name="data">The data. Can't be null.</param>
+ /// <param name="cancellationToken">Cancellation token.</param>
+ /// <returns>The tag of the created entry.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="data"/> is null.</exception>
+ public Task<string> RetainEntryAsync(byte[] data, CancellationToken cancellationToken = default);
+
+ /// <summary>
+ /// Decrease the the ref count of the entry.
+ /// Remove it if ref count is zero.
+ /// </summary>
+ /// <param name="tag">The tag of the entry.</param>
+ /// <param name="cancellationToken">Cancellation token.</param>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="tag"/> is null.</exception>
+ /// <remarks>
+ /// It's no-op if entry with tag does not exist.
+ /// </remarks>
+ public Task FreeEntryAsync(string tag, CancellationToken cancellationToken = default);
+
+ /// <summary>
+ /// Retrieve the entry with given tag. If not exist, returns null.
+ /// </summary>
+ /// <param name="tag">The tag of the entry.</param>
+ /// <param name="cancellationToken">Cancellation token.</param>
+ /// <returns>The data of the entry. If not exist, returns null.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="tag"/> is null.</exception>
+ public Task<byte[]?> GetEntryAsync(string tag, CancellationToken cancellationToken = default);
+ }
+}
diff --git a/BackEnd/Timeline/Services/Data/IETagGenerator.cs b/BackEnd/Timeline/Services/Data/IETagGenerator.cs
new file mode 100644
index 00000000..fa81c449
--- /dev/null
+++ b/BackEnd/Timeline/Services/Data/IETagGenerator.cs
@@ -0,0 +1,19 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Timeline.Services.Data
+{
+ public interface IETagGenerator
+ {
+ /// <summary>
+ /// Generate a etag for given source.
+ /// </summary>
+ /// <param name="source">The source data.</param>
+ /// <param name="cancellationToken">Cancellation token.</param>
+ /// <returns>The generated etag.</returns>
+ /// <exception cref="ArgumentNullException">Thrown if <paramref name="source"/> is null.</exception>
+ /// <remarks>This function must guarantee the same result with equal given source.</remarks>
+ Task<string> GenerateETagAsync(byte[] source, CancellationToken cancellationToken = default);
+ }
+}
diff --git a/BackEnd/Timeline/Services/Data/Resource.Designer.cs b/BackEnd/Timeline/Services/Data/Resource.Designer.cs
new file mode 100644
index 00000000..46b9eaf0
--- /dev/null
+++ b/BackEnd/Timeline/Services/Data/Resource.Designer.cs
@@ -0,0 +1,117 @@
+//------------------------------------------------------------------------------
+// <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.Services.Data {
+ 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 Resource {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resource() {
+ }
+
+ /// <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.Services.Data.Resource", typeof(Resource).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 Can&apos;t get data entry of tag {0}. {1}.
+ /// </summary>
+ internal static string GetEntryAndCheckNotExist {
+ get {
+ return ResourceManager.GetString("GetEntryAndCheckNotExist", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Decrease ref count of existing entry with tag {0}..
+ /// </summary>
+ internal static string LogDataManagerFreeEntryDecreaseRefCount {
+ get {
+ return ResourceManager.GetString("LogDataManagerFreeEntryDecreaseRefCount", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Attempt to free an entry that does not exist with tag {0}..
+ /// </summary>
+ internal static string LogDataManagerFreeEntryNotExist {
+ get {
+ return ResourceManager.GetString("LogDataManagerFreeEntryNotExist", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Remove a entry with tag {0}..
+ /// </summary>
+ internal static string LogDataManagerFreeEntryRemove {
+ get {
+ return ResourceManager.GetString("LogDataManagerFreeEntryRemove", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Add ref count of existing entry with tag {0}..
+ /// </summary>
+ internal static string LogDataManagerRetainEntryAddRefCount {
+ get {
+ return ResourceManager.GetString("LogDataManagerRetainEntryAddRefCount", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Create a new entry with tag {0}..
+ /// </summary>
+ internal static string LogDataManagerRetainEntryCreate {
+ get {
+ return ResourceManager.GetString("LogDataManagerRetainEntryCreate", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Services/Data/Resource.resx b/BackEnd/Timeline/Services/Data/Resource.resx
new file mode 100644
index 00000000..6783efae
--- /dev/null
+++ b/BackEnd/Timeline/Services/Data/Resource.resx
@@ -0,0 +1,138 @@
+<?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="GetEntryAndCheckNotExist" xml:space="preserve">
+ <value>Can't get data entry of tag {0}. {1}</value>
+ </data>
+ <data name="LogDataManagerFreeEntryDecreaseRefCount" xml:space="preserve">
+ <value>Decrease ref count of existing entry with tag {0}.</value>
+ </data>
+ <data name="LogDataManagerFreeEntryNotExist" xml:space="preserve">
+ <value>Attempt to free an entry that does not exist with tag {0}.</value>
+ </data>
+ <data name="LogDataManagerFreeEntryRemove" xml:space="preserve">
+ <value>Remove a entry with tag {0}.</value>
+ </data>
+ <data name="LogDataManagerRetainEntryAddRefCount" xml:space="preserve">
+ <value>Add ref count of existing entry with tag {0}.</value>
+ </data>
+ <data name="LogDataManagerRetainEntryCreate" xml:space="preserve">
+ <value>Create a new entry with tag {0}.</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/BackEnd/Timeline/Services/DatabaseManagement/DatabaseBackupService.cs b/BackEnd/Timeline/Services/DatabaseManagement/DatabaseBackupService.cs
index c00b5f95..718edcb1 100644
--- a/BackEnd/Timeline/Services/DatabaseManagement/DatabaseBackupService.cs
+++ b/BackEnd/Timeline/Services/DatabaseManagement/DatabaseBackupService.cs
@@ -1,4 +1,5 @@
using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
using System.Globalization;
using System.IO;
using System.Threading;
@@ -7,19 +8,16 @@ using Timeline.Entities;
namespace Timeline.Services.DatabaseManagement
{
- public interface IDatabaseBackupService
- {
- Task BackupAsync(CancellationToken cancellationToken = default);
- }
-
public class DatabaseBackupService : IDatabaseBackupService
{
+ private readonly ILogger<DatabaseBackupService> _logger;
private readonly DatabaseContext _database;
private readonly IPathProvider _pathProvider;
private readonly IClock _clock;
- public DatabaseBackupService(DatabaseContext database, IPathProvider pathProvider, IClock clock)
+ public DatabaseBackupService(ILogger<DatabaseBackupService> logger, DatabaseContext database, IPathProvider pathProvider, IClock clock)
{
+ _logger = logger;
_database = database;
_pathProvider = pathProvider;
_clock = clock;
@@ -32,6 +30,7 @@ namespace Timeline.Services.DatabaseManagement
var fileName = _clock.GetCurrentTime().ToString("yyyy-MM-ddTHH-mm-ss", CultureInfo.InvariantCulture);
var path = Path.Combine(backupDirPath, fileName);
await _database.Database.ExecuteSqlInterpolatedAsync($"VACUUM INTO {path}", cancellationToken);
+ _logger.LogWarning(Resource.DatabaseBackupServiceFinishBackup, path);
}
}
}
diff --git a/BackEnd/Timeline/Services/DatabaseManagement/DatabaseCustomMigrator.cs b/BackEnd/Timeline/Services/DatabaseManagement/DatabaseCustomMigrator.cs
index 2180ad40..20e2c074 100644
--- a/BackEnd/Timeline/Services/DatabaseManagement/DatabaseCustomMigrator.cs
+++ b/BackEnd/Timeline/Services/DatabaseManagement/DatabaseCustomMigrator.cs
@@ -7,17 +7,12 @@ using Timeline.Entities;
namespace Timeline.Services.DatabaseManagement
{
- public interface IDatabaseCustomMigrator
- {
- Task MigrateAsync(CancellationToken cancellationToken = default);
- }
-
public class DatabaseCustomMigrator : IDatabaseCustomMigrator
{
- private IEnumerable<IDatabaseCustomMigration> _migrations;
- private DatabaseContext _database;
+ private readonly IEnumerable<IDatabaseCustomMigration> _migrations;
+ private readonly DatabaseContext _database;
- private ILogger<DatabaseCustomMigrator> _logger;
+ private readonly ILogger<DatabaseCustomMigrator> _logger;
public DatabaseCustomMigrator(IEnumerable<IDatabaseCustomMigration> migrations, DatabaseContext database, ILogger<DatabaseCustomMigrator> logger)
{
@@ -33,11 +28,11 @@ namespace Timeline.Services.DatabaseManagement
var name = migration.GetName();
var isApplied = await _database.Migrations.AnyAsync(m => m.Name == name, cancellationToken);
- _logger.LogInformation("Found custom migration '{0}'. Applied: {1}.", name, isApplied);
+ _logger.LogInformation(Resource.DatabaseCustomMigratorFoundMigration, name, isApplied);
if (!isApplied)
{
- _logger.LogWarning("Begin custom migration '{0}'.", name);
+ _logger.LogWarning(Resource.DatabaseCustomMigratorBeginMigration, name);
await using var transaction = await _database.Database.BeginTransactionAsync(cancellationToken);
@@ -48,7 +43,7 @@ namespace Timeline.Services.DatabaseManagement
await transaction.CommitAsync(cancellationToken);
- _logger.LogWarning("End custom migration '{0}'.", name);
+ _logger.LogWarning(Resource.DatabaseCustomMigratorFinishMigration, name);
}
}
}
diff --git a/BackEnd/Timeline/Services/DatabaseManagement/IDatabaseBackupService.cs b/BackEnd/Timeline/Services/DatabaseManagement/IDatabaseBackupService.cs
new file mode 100644
index 00000000..88f3d9b6
--- /dev/null
+++ b/BackEnd/Timeline/Services/DatabaseManagement/IDatabaseBackupService.cs
@@ -0,0 +1,10 @@
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Timeline.Services.DatabaseManagement
+{
+ public interface IDatabaseBackupService
+ {
+ Task BackupAsync(CancellationToken cancellationToken = default);
+ }
+}
diff --git a/BackEnd/Timeline/Services/DatabaseManagement/IDatabaseCustomMigration.cs b/BackEnd/Timeline/Services/DatabaseManagement/IDatabaseCustomMigration.cs
index 7300ccbd..a38dcb99 100644
--- a/BackEnd/Timeline/Services/DatabaseManagement/IDatabaseCustomMigration.cs
+++ b/BackEnd/Timeline/Services/DatabaseManagement/IDatabaseCustomMigration.cs
@@ -7,6 +7,15 @@ namespace Timeline.Services.DatabaseManagement
public interface IDatabaseCustomMigration
{
string GetName();
+
+ /// <summary>
+ /// Execute the migration on database.
+ /// </summary>
+ /// <param name="database">The database.</param>
+ /// <param name="cancellationToken">Cancellation token.</param>
+ /// <remarks>
+ /// Do not create transaction since the migrator will take care of transaction.
+ /// </remarks>
Task ExecuteAsync(DatabaseContext database, CancellationToken cancellationToken = default);
}
}
diff --git a/BackEnd/Timeline/Services/DatabaseManagement/IDatabaseCustomMigrator.cs b/BackEnd/Timeline/Services/DatabaseManagement/IDatabaseCustomMigrator.cs
new file mode 100644
index 00000000..5eb43b92
--- /dev/null
+++ b/BackEnd/Timeline/Services/DatabaseManagement/IDatabaseCustomMigrator.cs
@@ -0,0 +1,10 @@
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Timeline.Services.DatabaseManagement
+{
+ public interface IDatabaseCustomMigrator
+ {
+ Task MigrateAsync(CancellationToken cancellationToken = default);
+ }
+}
diff --git a/BackEnd/Timeline/Services/DatabaseManagement/MigationServiceCollectionExtensions.cs b/BackEnd/Timeline/Services/DatabaseManagement/MigationServiceCollectionExtensions.cs
index d1f9b51c..8269ce28 100644
--- a/BackEnd/Timeline/Services/DatabaseManagement/MigationServiceCollectionExtensions.cs
+++ b/BackEnd/Timeline/Services/DatabaseManagement/MigationServiceCollectionExtensions.cs
@@ -1,14 +1,17 @@
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Timeline.Services.DatabaseManagement
{
- public static class DatabaseManagementServiceCollectionExtensions
+ public static class DatabaseManagementServicesServiceCollectionExtensions
{
public static IServiceCollection AddDatabaseManagementService(this IServiceCollection services)
{
- services.AddScoped<IDatabaseCustomMigrator, DatabaseCustomMigrator>();
+ services.TryAddScoped<IDatabaseCustomMigrator, DatabaseCustomMigrator>();
services.AddScoped<IDatabaseCustomMigration, TimelinePostContentToDataMigration>();
- services.AddScoped<IDatabaseBackupService, DatabaseBackupService>();
+
+ services.TryAddScoped<IDatabaseBackupService, DatabaseBackupService>();
+
services.AddHostedService<DatabaseManagementService>();
return services;
}
diff --git a/BackEnd/Timeline/Resources/Services/DataManager.Designer.cs b/BackEnd/Timeline/Services/DatabaseManagement/Resource.Designer.cs
index 0872059a..c0e61a3f 100644
--- a/BackEnd/Timeline/Resources/Services/DataManager.Designer.cs
+++ b/BackEnd/Timeline/Services/DatabaseManagement/Resource.Designer.cs
@@ -8,7 +8,7 @@
// </auto-generated>
//------------------------------------------------------------------------------
-namespace Timeline.Resources.Services {
+namespace Timeline.Services.DatabaseManagement {
using System;
@@ -22,14 +22,14 @@ namespace Timeline.Resources.Services {
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
- internal class DataManager {
+ internal class Resource {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
- internal DataManager() {
+ internal Resource() {
}
/// <summary>
@@ -39,7 +39,7 @@ namespace Timeline.Resources.Services {
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.DataManager", typeof(DataManager).Assembly);
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Services.DatabaseManagement.Resource", typeof(Resource).Assembly);
resourceMan = temp;
}
return resourceMan;
@@ -61,11 +61,47 @@ namespace Timeline.Resources.Services {
}
/// <summary>
- /// Looks up a localized string similar to Entry with given tag does not exist..
+ /// Looks up a localized string similar to Database backup finished with output file at {0}..
/// </summary>
- internal static string ExceptionEntryNotExist {
+ internal static string DatabaseBackupServiceFinishBackup {
get {
- return ResourceManager.GetString("ExceptionEntryNotExist", resourceCulture);
+ return ResourceManager.GetString("DatabaseBackupServiceFinishBackup", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Begin custom migration &apos;{0}&apos;..
+ /// </summary>
+ internal static string DatabaseCustomMigratorBeginMigration {
+ get {
+ return ResourceManager.GetString("DatabaseCustomMigratorBeginMigration", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to End custom migration &apos;{0}&apos;..
+ /// </summary>
+ internal static string DatabaseCustomMigratorFinishMigration {
+ get {
+ return ResourceManager.GetString("DatabaseCustomMigratorFinishMigration", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Found custom migration &apos;{0}&apos;. Applied: {1}..
+ /// </summary>
+ internal static string DatabaseCustomMigratorFoundMigration {
+ get {
+ return ResourceManager.GetString("DatabaseCustomMigratorFoundMigration", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Old image content does not have corresponding data with the tag..
+ /// </summary>
+ internal static string TimelinePostContentToDataMigrationImageNoData {
+ get {
+ return ResourceManager.GetString("TimelinePostContentToDataMigrationImageNoData", resourceCulture);
}
}
}
diff --git a/BackEnd/Timeline/Resources/Services/DataManager.resx b/BackEnd/Timeline/Services/DatabaseManagement/Resource.resx
index 688e0e96..9480ce64 100644
--- a/BackEnd/Timeline/Resources/Services/DataManager.resx
+++ b/BackEnd/Timeline/Services/DatabaseManagement/Resource.resx
@@ -117,7 +117,19 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
- <data name="ExceptionEntryNotExist" xml:space="preserve">
- <value>Entry with given tag does not exist.</value>
+ <data name="DatabaseBackupServiceFinishBackup" xml:space="preserve">
+ <value>Database backup finished with output file at {0}.</value>
+ </data>
+ <data name="DatabaseCustomMigratorBeginMigration" xml:space="preserve">
+ <value>Begin custom migration '{0}'.</value>
+ </data>
+ <data name="DatabaseCustomMigratorFinishMigration" xml:space="preserve">
+ <value>End custom migration '{0}'.</value>
+ </data>
+ <data name="DatabaseCustomMigratorFoundMigration" xml:space="preserve">
+ <value>Found custom migration '{0}'. Applied: {1}.</value>
+ </data>
+ <data name="TimelinePostContentToDataMigrationImageNoData" xml:space="preserve">
+ <value>Old image content does not have corresponding data with the tag.</value>
</data>
</root> \ No newline at end of file
diff --git a/BackEnd/Timeline/Services/DatabaseManagement/TimelinePostContentToDataMigration.cs b/BackEnd/Timeline/Services/DatabaseManagement/TimelinePostContentToDataMigration.cs
index f9a3418b..f6662a97 100644
--- a/BackEnd/Timeline/Services/DatabaseManagement/TimelinePostContentToDataMigration.cs
+++ b/BackEnd/Timeline/Services/DatabaseManagement/TimelinePostContentToDataMigration.cs
@@ -35,7 +35,7 @@ namespace Timeline.Services.DatabaseManagement
{
if (postEntity.ContentType == "text")
{
- var tag = await _dataManager.RetainEntry(Encoding.UTF8.GetBytes(postEntity.Content));
+ var tag = await _dataManager.RetainEntryAsync(Encoding.UTF8.GetBytes(postEntity.Content), cancellationToken);
database.TimelinePostData.Add(new TimelinePostDataEntity
{
DataTag = tag,
@@ -47,7 +47,7 @@ namespace Timeline.Services.DatabaseManagement
}
else
{
- var data = await _dataManager.GetEntryAndCheck(postEntity.Content, "Old image content does not have corresponding data with the tag.");
+ var data = await _dataManager.GetEntryAndCheck(postEntity.Content, Resource.TimelinePostContentToDataMigrationImageNoData, cancellationToken);
var format = Image.DetectFormat(data);
database.TimelinePostData.Add(new TimelinePostDataEntity
{
diff --git a/BackEnd/Timeline/Services/Imaging/IImageService.cs b/BackEnd/Timeline/Services/Imaging/IImageService.cs
new file mode 100644
index 00000000..9e595ff8
--- /dev/null
+++ b/BackEnd/Timeline/Services/Imaging/IImageService.cs
@@ -0,0 +1,32 @@
+using SixLabors.ImageSharp.Formats;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Timeline.Services.Imaging
+{
+ public interface IImageService
+ {
+ /// <summary>
+ /// Detect the format of a image.
+ /// </summary>
+ /// <param name="data"></param>
+ /// <param name="cancellationToken"></param>
+ /// <returns>The image format.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="data"/> is null.</exception>
+ /// <exception cref="ImageException">Thrown when image data can't be detected.</exception>
+ Task<IImageFormat> DetectFormatAsync(byte[] data, CancellationToken cancellationToken = default);
+
+ /// <summary>
+ /// Validate a image data.
+ /// </summary>
+ /// <param name="data">The data of the image. Can't be null.</param>
+ /// <param name="requestType">If not null, the real image format will be check against the requested format and throw if not match. If null, then do not check.</param>
+ /// <param name="square">If true, image must be square.</param>
+ /// <param name="cancellationToken">Cancellation token.</param>
+ /// <returns>The format.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="data"/> is null.</exception>
+ /// <exception cref="ImageException">Thrown when image data can't be decoded or real type does not match request type or image is not square when required.</exception>
+ Task<IImageFormat> ValidateAsync(byte[] data, string? requestType = null, bool square = false, CancellationToken cancellationToken = default);
+ }
+}
diff --git a/BackEnd/Timeline/Services/Imaging/ImageException.cs b/BackEnd/Timeline/Services/Imaging/ImageException.cs
index 926ecc0a..12aefa0a 100644
--- a/BackEnd/Timeline/Services/Imaging/ImageException.cs
+++ b/BackEnd/Timeline/Services/Imaging/ImageException.cs
@@ -19,7 +19,7 @@ namespace Timeline.Services.Imaging
/// <summary>
/// Image is not of required size.
/// </summary>
- NotSquare,
+ BadSize,
/// <summary>
/// Other unknown errer.
/// </summary>
@@ -42,16 +42,13 @@ namespace Timeline.Services.Imaging
{
ErrorReason.CantDecode => Resource.ExceptionImageReasonCantDecode,
ErrorReason.UnmatchedFormat => Resource.ExceptionImageReasonUnmatchedFormat,
- ErrorReason.NotSquare => Resource.ExceptionImageReasonBadSize,
+ ErrorReason.BadSize => Resource.ExceptionImageReasonBadSize,
_ => Resource.ExceptionImageReasonUnknownError
});
public ErrorReason Error { get; }
-#pragma warning disable CA1819 // Properties should not return arrays
public byte[]? ImageData { get; }
-#pragma warning restore CA1819 // Properties should not return arrays
public string? RequestType { get; }
-
// This field will be null if decoding failed.
public string? RealType { get; }
}
diff --git a/BackEnd/Timeline/Services/Imaging/ImageService.cs b/BackEnd/Timeline/Services/Imaging/ImageService.cs
new file mode 100644
index 00000000..9fefe3e9
--- /dev/null
+++ b/BackEnd/Timeline/Services/Imaging/ImageService.cs
@@ -0,0 +1,54 @@
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.Formats;
+using System;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Timeline.Services.Imaging
+{
+
+ public class ImageService : IImageService
+ {
+ public async Task<IImageFormat> DetectFormatAsync(byte[] data, CancellationToken cancellationToken = default)
+ {
+ if (data == null)
+ throw new ArgumentNullException(nameof(data));
+
+ var format = await Task.Run(() =>
+ {
+ var format = Image.DetectFormat(data);
+ if (format is null)
+ {
+ throw new ImageException(ImageException.ErrorReason.CantDecode, data, null, null, null);
+ }
+ return format;
+ }, cancellationToken);
+ return format;
+ }
+
+ public async Task<IImageFormat> ValidateAsync(byte[] data, string? requestType = null, bool square = false, CancellationToken cancellationToken = default)
+ {
+ if (data == null)
+ throw new ArgumentNullException(nameof(data));
+
+ var format = await Task.Run(() =>
+ {
+ try
+ {
+ using var image = Image.Load(data, out IImageFormat format);
+ if (requestType != null && !format.MimeTypes.Contains(requestType))
+ throw new ImageException(ImageException.ErrorReason.UnmatchedFormat, data, requestType, format.DefaultMimeType);
+ if (square && image.Width != image.Height)
+ throw new ImageException(ImageException.ErrorReason.BadSize, data, requestType, format.DefaultMimeType);
+ return format;
+ }
+ catch (UnknownImageFormatException e)
+ {
+ throw new ImageException(ImageException.ErrorReason.CantDecode, data, requestType, null, null, e);
+ }
+ }, cancellationToken);
+ return format;
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Services/Imaging/ImageServicesServiceCollectionExtensions.cs b/BackEnd/Timeline/Services/Imaging/ImageServicesServiceCollectionExtensions.cs
new file mode 100644
index 00000000..7906b35e
--- /dev/null
+++ b/BackEnd/Timeline/Services/Imaging/ImageServicesServiceCollectionExtensions.cs
@@ -0,0 +1,14 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+
+namespace Timeline.Services.Imaging
+{
+ public static class ImageServicesServiceCollectionExtensions
+ {
+ public static IServiceCollection AddImageServices(this IServiceCollection services)
+ {
+ services.TryAddTransient<IImageService, ImageService>();
+ return services;
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Services/Imaging/ImageValidator.cs b/BackEnd/Timeline/Services/Imaging/ImageValidator.cs
deleted file mode 100644
index b4ae68dc..00000000
--- a/BackEnd/Timeline/Services/Imaging/ImageValidator.cs
+++ /dev/null
@@ -1,53 +0,0 @@
-using SixLabors.ImageSharp;
-using SixLabors.ImageSharp.Formats;
-using System;
-using System.Linq;
-using System.Threading.Tasks;
-
-namespace Timeline.Services.Imaging
-{
- public interface IImageValidator
- {
- /// <summary>
- /// Validate a image data.
- /// </summary>
- /// <param name="data">The data of the image. Can't be null.</param>
- /// <param name="requestType">If not null, the real image format will be check against the requested format and throw if not match. If null, then do not check.</param>
- /// <param name="square">If true, image must be square.</param>
- /// <returns>The format.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="data"/> is null.</exception>
- /// <exception cref="ImageException">Thrown when image data can't be decoded or real type does not match request type or image is not square when required.</exception>
- Task<IImageFormat> Validate(byte[] data, string? requestType = null, bool square = false);
- }
-
- public class ImageValidator : IImageValidator
- {
- public ImageValidator()
- {
- }
-
- public async Task<IImageFormat> Validate(byte[] data, string? requestType = null, bool square = false)
- {
- if (data == null)
- throw new ArgumentNullException(nameof(data));
-
- var format = await Task.Run(() =>
- {
- try
- {
- using var image = Image.Load(data, out IImageFormat format);
- if (requestType != null && !format.MimeTypes.Contains(requestType))
- throw new ImageException(ImageException.ErrorReason.UnmatchedFormat, data, requestType, format.DefaultMimeType);
- if (square && image.Width != image.Height)
- throw new ImageException(ImageException.ErrorReason.NotSquare, data, requestType, format.DefaultMimeType);
- return format;
- }
- catch (UnknownImageFormatException e)
- {
- throw new ImageException(ImageException.ErrorReason.CantDecode, data, requestType, null, null, e);
- }
- });
- return format;
- }
- }
-}
diff --git a/BackEnd/Timeline/Services/Timeline/TimelinePostService.cs b/BackEnd/Timeline/Services/Timeline/TimelinePostService.cs
index 073fffdf..6a6273c5 100644
--- a/BackEnd/Timeline/Services/Timeline/TimelinePostService.cs
+++ b/BackEnd/Timeline/Services/Timeline/TimelinePostService.cs
@@ -106,7 +106,7 @@ namespace Timeline.Services.Timeline
/// <exception cref="ArgumentException">Thrown when <paramref name="request"/> is of invalid format.</exception>
/// <exception cref="TimelineNotExistException">Thrown when timeline does not exist.</exception>
/// <exception cref="UserNotExistException">Thrown if user of <paramref name="authorId"/> does not exist.</exception>
- /// <exception cref="ImageException">Thrown if data is not a image. Validated by <see cref="ImageValidator"/>.</exception>
+ /// <exception cref="ImageException">Thrown if data is not a image. Validated by <see cref="ImageService"/>.</exception>
Task<TimelinePostEntity> CreatePost(long timelineId, long authorId, TimelinePostCreateRequest request);
/// <summary>
@@ -167,11 +167,11 @@ namespace Timeline.Services.Timeline
private readonly IBasicTimelineService _basicTimelineService;
private readonly IBasicUserService _basicUserService;
private readonly IDataManager _dataManager;
- private readonly IImageValidator _imageValidator;
+ private readonly IImageService _imageValidator;
private readonly IClock _clock;
private readonly ColorValidator _colorValidator = new ColorValidator();
- public TimelinePostService(ILogger<TimelinePostService> logger, DatabaseContext database, IBasicTimelineService basicTimelineService, IBasicUserService basicUserService, IDataManager dataManager, IImageValidator imageValidator, IClock clock)
+ public TimelinePostService(ILogger<TimelinePostService> logger, DatabaseContext database, IBasicTimelineService basicTimelineService, IBasicUserService basicUserService, IDataManager dataManager, IImageService imageValidator, IClock clock)
{
_logger = logger;
_database = database;
@@ -309,7 +309,7 @@ namespace Timeline.Services.Timeline
case MimeTypes.ImageWebp:
try
{
- await _imageValidator.Validate(data.Data, data.ContentType);
+ await _imageValidator.ValidateAsync(data.Data, data.ContentType);
}
catch (ImageException e)
{
@@ -363,7 +363,7 @@ namespace Timeline.Services.Timeline
{
var data = request.DataList[index];
- var tag = await _dataManager.RetainEntry(data.Data);
+ var tag = await _dataManager.RetainEntryAsync(data.Data);
_database.TimelinePostData.Add(new TimelinePostDataEntity
{
@@ -438,7 +438,7 @@ namespace Timeline.Services.Timeline
foreach (var dataEntity in dataEntities)
{
- await _dataManager.FreeEntry(dataEntity.DataTag);
+ await _dataManager.FreeEntryAsync(dataEntity.DataTag);
}
_database.TimelinePostData.RemoveRange(dataEntities);
diff --git a/BackEnd/Timeline/Services/User/UserAvatarService.cs b/BackEnd/Timeline/Services/User/UserAvatarService.cs
index 0a4b7438..e18a0560 100644
--- a/BackEnd/Timeline/Services/User/UserAvatarService.cs
+++ b/BackEnd/Timeline/Services/User/UserAvatarService.cs
@@ -95,7 +95,7 @@ namespace Timeline.Services.User
if (_cacheData == null || File.GetLastWriteTime(path) > _cacheDigest!.LastModified)
{
var data = await File.ReadAllBytesAsync(path);
- _cacheDigest = new CacheableDataDigest(await _eTagGenerator.Generate(data), File.GetLastWriteTime(path));
+ _cacheDigest = new CacheableDataDigest(await _eTagGenerator.GenerateETagAsync(data), File.GetLastWriteTime(path));
Image.Identify(data, out var format);
_cacheData = new ByteData(data, format.DefaultMimeType);
}
@@ -120,7 +120,7 @@ namespace Timeline.Services.User
private readonly DatabaseContext _database;
private readonly IBasicUserService _basicUserService;
private readonly IDefaultUserAvatarProvider _defaultUserAvatarProvider;
- private readonly IImageValidator _imageValidator;
+ private readonly IImageService _imageValidator;
private readonly IDataManager _dataManager;
private readonly IClock _clock;
@@ -129,7 +129,7 @@ namespace Timeline.Services.User
DatabaseContext database,
IBasicUserService basicUserService,
IDefaultUserAvatarProvider defaultUserAvatarProvider,
- IImageValidator imageValidator,
+ IImageService imageValidator,
IDataManager dataManager,
IClock clock)
{
@@ -192,7 +192,7 @@ namespace Timeline.Services.User
if (avatar is null)
throw new ArgumentNullException(nameof(avatar));
- await _imageValidator.Validate(avatar.Data, avatar.ContentType, true);
+ await _imageValidator.ValidateAsync(avatar.Data, avatar.ContentType, true);
await _basicUserService.ThrowIfUserNotExist(userId);
@@ -200,7 +200,7 @@ namespace Timeline.Services.User
await using var transaction = await _database.Database.BeginTransactionAsync();
- var tag = await _dataManager.RetainEntry(avatar.Data);
+ var tag = await _dataManager.RetainEntryAsync(avatar.Data);
var now = _clock.GetCurrentTime();
@@ -218,7 +218,7 @@ namespace Timeline.Services.User
else
{
if (entity.DataTag is not null)
- await _dataManager.FreeEntry(entity.DataTag);
+ await _dataManager.FreeEntryAsync(entity.DataTag);
entity.DataTag = tag;
entity.Type = avatar.ContentType;
@@ -243,7 +243,7 @@ namespace Timeline.Services.User
await using var transaction = await _database.Database.BeginTransactionAsync();
- await _dataManager.FreeEntry(entity.DataTag);
+ await _dataManager.FreeEntryAsync(entity.DataTag);
entity.DataTag = null;
entity.Type = null;
diff --git a/BackEnd/Timeline/Startup.cs b/BackEnd/Timeline/Startup.cs
index 0d7bc6b3..58438d4c 100644
--- a/BackEnd/Timeline/Startup.cs
+++ b/BackEnd/Timeline/Startup.cs
@@ -88,22 +88,14 @@ namespace Timeline
services.AddAuthorization();
services.AddSingleton<IAuthorizationPolicyProvider, PermissionPolicyProvider>();
- // TODO: Remove this.
- services.TryAddSingleton<IActionContextAccessor, ActionContextAccessor>();
-
- services.AddSingleton<IPathProvider, PathProvider>();
-
+ services.AddBasicServices();
services.AddDatabaseManagementService();
+ services.AddDataServices();
+ services.AddImageServices();
services.AddAutoMapper(GetType().Assembly);
services.AddMappers();
- services.AddTransient<IClock, Clock>();
-
- services.AddScoped<IETagGenerator, ETagGenerator>();
- services.AddScoped<IDataManager, DataManager>();
- services.AddScoped<IImageValidator, ImageValidator>();
-
services.AddDbContext<DatabaseContext>((services, options) =>
{
var pathProvider = services.GetRequiredService<IPathProvider>();
diff --git a/BackEnd/Timeline/Timeline.csproj b/BackEnd/Timeline/Timeline.csproj
index 47713284..15771977 100644
--- a/BackEnd/Timeline/Timeline.csproj
+++ b/BackEnd/Timeline/Timeline.csproj
@@ -123,11 +123,6 @@
<AutoGen>True</AutoGen>
<DependentUpon>Validator.resx</DependentUpon>
</Compile>
- <Compile Update="Resources\Services\DataManager.Designer.cs">
- <DesignTime>True</DesignTime>
- <AutoGen>True</AutoGen>
- <DependentUpon>DataManager.resx</DependentUpon>
- </Compile>
<Compile Update="Resources\Services\TimelineService.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
@@ -148,6 +143,16 @@
<AutoGen>True</AutoGen>
<DependentUpon>UserTokenService.resx</DependentUpon>
</Compile>
+ <Compile Update="Services\DatabaseManagement\Resource.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>Resource.resx</DependentUpon>
+ </Compile>
+ <Compile Update="Services\Data\Resource.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>Resource.resx</DependentUpon>
+ </Compile>
<Compile Update="Services\Imaging\Resource.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
@@ -237,10 +242,6 @@
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Validator.Designer.cs</LastGenOutput>
</EmbeddedResource>
- <EmbeddedResource Update="Resources\Services\DataManager.resx">
- <Generator>ResXFileCodeGenerator</Generator>
- <LastGenOutput>DataManager.Designer.cs</LastGenOutput>
- </EmbeddedResource>
<EmbeddedResource Update="Resources\Services\TimelineService.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>TimelineService.Designer.cs</LastGenOutput>
@@ -257,6 +258,14 @@
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>UserTokenService.Designer.cs</LastGenOutput>
</EmbeddedResource>
+ <EmbeddedResource Update="Services\DatabaseManagement\Resource.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>Resource.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Update="Services\Data\Resource.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>Resource.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
<EmbeddedResource Update="Services\Imaging\Resource.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resource.Designer.cs</LastGenOutput>