From 657fb589137099794e58fbd35beb7d942b376965 Mon Sep 17 00:00:00 2001 From: crupest Date: Sun, 25 Apr 2021 21:20:04 +0800 Subject: ... --- BackEnd/Timeline/Services/Data/DataManager.cs | 138 ++++++++++++++++++++++++ BackEnd/Timeline/Services/Data/ETagGenerator.cs | 45 ++++++++ 2 files changed, 183 insertions(+) create mode 100644 BackEnd/Timeline/Services/Data/DataManager.cs create mode 100644 BackEnd/Timeline/Services/Data/ETagGenerator.cs (limited to 'BackEnd/Timeline/Services/Data') diff --git a/BackEnd/Timeline/Services/Data/DataManager.cs b/BackEnd/Timeline/Services/Data/DataManager.cs new file mode 100644 index 00000000..d9a4491d --- /dev/null +++ b/BackEnd/Timeline/Services/Data/DataManager.cs @@ -0,0 +1,138 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Linq; +using System.Threading.Tasks; +using Timeline.Entities; + +namespace Timeline.Services.Data +{ + /// + /// A data manager controlling data. + /// + /// + /// 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. + /// + public interface IDataManager + { + /// + /// Saves the data to a new entry if it does not exist, + /// increases its ref count and returns a tag to the entry. + /// + /// The data. Can't be null. + /// The tag of the created entry. + /// Thrown when is null. + public Task RetainEntry(byte[] data); + + /// + /// Decrease the the ref count of the entry. + /// Remove it if ref count is zero. + /// + /// The tag of the entry. + /// Thrown when is null. + /// + /// It's no-op if entry with tag does not exist. + /// + public Task FreeEntry(string tag); + + /// + /// Retrieve the entry with given tag. If not exist, returns null. + /// + /// The tag of the entry. + /// The data of the entry. If not exist, returns null. + /// Thrown when is null. + public Task GetEntry(string tag); + } + + public class DataManager : IDataManager + { + private readonly DatabaseContext _database; + private readonly IETagGenerator _eTagGenerator; + + public DataManager(DatabaseContext database, IETagGenerator eTagGenerator) + { + _database = database; + _eTagGenerator = eTagGenerator; + } + + public async Task RetainEntry(byte[] data) + { + if (data == null) + throw new ArgumentNullException(nameof(data)); + + var tag = await _eTagGenerator.Generate(data); + + var entity = await _database.Data.Where(d => d.Tag == tag).SingleOrDefaultAsync(); + + if (entity == null) + { + entity = new DataEntity + { + Tag = tag, + Data = data, + Ref = 1 + }; + _database.Data.Add(entity); + } + else + { + entity.Ref += 1; + } + + await _database.SaveChangesAsync(); + + return tag; + } + + public async Task FreeEntry(string tag) + { + if (tag == null) + throw new ArgumentNullException(nameof(tag)); + + var entity = await _database.Data.Where(d => d.Tag == tag).SingleOrDefaultAsync(); + + if (entity != null) + { + if (entity.Ref == 1) + { + _database.Data.Remove(entity); + } + else + { + entity.Ref -= 1; + } + + await _database.SaveChangesAsync(); + } + } + + public async Task GetEntry(string tag) + { + if (tag == null) + throw new ArgumentNullException(nameof(tag)); + + var entity = await _database.Data.Where(d => d.Tag == tag).Select(d => new { d.Data }).SingleOrDefaultAsync(); + + if (entity is null) + return null; + + return entity.Data; + } + } + + public static class DataManagerExtensions + { + /// + /// Try to get an entry and throw if not exist. + /// + public static async Task 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/ETagGenerator.cs b/BackEnd/Timeline/Services/Data/ETagGenerator.cs new file mode 100644 index 00000000..847c120b --- /dev/null +++ b/BackEnd/Timeline/Services/Data/ETagGenerator.cs @@ -0,0 +1,45 @@ +using System; +using System.Security.Cryptography; +using System.Threading.Tasks; + +namespace Timeline.Services.Data +{ + public interface IETagGenerator + { + /// + /// Generate a etag for given source. + /// + /// The source data. + /// The generated etag. + /// Thrown if is null. + Task Generate(byte[] source); + } + + 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(); + } + + public Task Generate(byte[] source) + { + if (source == null) + throw new ArgumentNullException(nameof(source)); + + return Task.Run(() => Convert.ToBase64String(_sha1.ComputeHash(source))); + } + + private bool _disposed; // To detect redundant calls + + public void Dispose() + { + if (_disposed) return; + _sha1.Dispose(); + _disposed = true; + } + } +} -- cgit v1.2.3