From 3ea363bbafcaccb74e9ff05611c58ee88506264e Mon Sep 17 00:00:00 2001 From: Matthias Beerens <3512339+Matthiee@users.noreply.github.com> Date: Sun, 22 Dec 2019 14:27:09 +0100 Subject: [PATCH 01/14] Add UpdateVersion + tests --- UpdateLib.Tests/Common/UpdateVersionTests.cs | 110 ++++++++ UpdateLib.Tests/UpdateLib.Tests.csproj | 4 + UpdateLib/Abstractions/IUpdater.cs | 8 +- UpdateLib/Core/CheckForUpdatesResult.cs | 10 + UpdateLib/Core/Enums/VersionLabel.cs | 10 + UpdateLib/Core/UpdateVersion.cs | 275 +++++++++++++++++++ UpdateLib/UpdateLib.csproj | 4 - UpdateLib/Updater.cs | 13 +- 8 files changed, 426 insertions(+), 8 deletions(-) create mode 100644 UpdateLib.Tests/Common/UpdateVersionTests.cs create mode 100644 UpdateLib/Core/CheckForUpdatesResult.cs create mode 100644 UpdateLib/Core/Enums/VersionLabel.cs create mode 100644 UpdateLib/Core/UpdateVersion.cs diff --git a/UpdateLib.Tests/Common/UpdateVersionTests.cs b/UpdateLib.Tests/Common/UpdateVersionTests.cs new file mode 100644 index 0000000..c45a539 --- /dev/null +++ b/UpdateLib.Tests/Common/UpdateVersionTests.cs @@ -0,0 +1,110 @@ +using System; +using UpdateLib.Core; +using UpdateLib.Core.Enums; +using Xunit; + +namespace UpdateLib.Tests.Common +{ + public class UpdateVersionTests + { + [Theory] + [InlineData("1.2.3-beta", 1, 2, 3, VersionLabel.Beta)] + [InlineData("1.2.3-rc", 1, 2, 3, VersionLabel.RC)] + [InlineData("1.2.3-alpha", 1, 2, 3, VersionLabel.Alpha)] + [InlineData("1.2.3", 1, 2, 3, VersionLabel.None)] + [InlineData("1.2", 1, 2, 0, VersionLabel.None)] + [InlineData("1.2-beta", 1, 2, 0, VersionLabel.Beta)] + [InlineData("1", 1, 0, 0, VersionLabel.None)] + [InlineData("1-rc", 1, 0, 0, VersionLabel.RC)] + public void TestTryParseGood(string input, int major, int minor, int patch, VersionLabel label) + { + var v = new UpdateVersion(input); + + Assert.Equal(major, v.Major); + Assert.Equal(minor, v.Minor); + Assert.Equal(patch, v.Patch); + Assert.Equal(label, v.Label); + } + + [Theory] + [InlineData("1-beta-alpha")] + [InlineData("1-xxx")] + [InlineData("xxx-1.2.3")] + [InlineData("1-2.3.4")] + [InlineData("blabla")] + public void TestTryParseBad(string input) + { + Assert.ThrowsAny(() => new UpdateVersion(input)); + } + + [Fact] + public void TestTryParseReturnsFalseInBadCase() + { + string input = "1.2.3.beta"; + + Assert.False(UpdateVersion.TryParse(input, out UpdateVersion _)); + } + + [Fact] + public void TestStringValue() + { + var v = new UpdateVersion(1, 2, 3, VersionLabel.RC); + + Assert.Equal("1.2.3-rc", v.Value); + + v.Value = "3.1.2"; + + Assert.Equal(3, v.Major); + Assert.Equal(1, v.Minor); + Assert.Equal(2, v.Patch); + Assert.Equal(VersionLabel.None, v.Label); + } + + [Fact] + public void ConstructorThrowsException() + { + Assert.Throws(() => new UpdateVersion(-1)); + Assert.Throws(() => new UpdateVersion(1, -1)); + Assert.Throws(() => new UpdateVersion(1, 1, -1)); + Assert.Throws(() => new UpdateVersion("blabla")); + } + + [Fact] + public void TestOperators() + { + UpdateVersion v1 = new UpdateVersion(1); + UpdateVersion v2 = new UpdateVersion(1); + UpdateVersion v3 = new UpdateVersion(1, 1); + UpdateVersion v4 = new UpdateVersion(1, 1, 1); + UpdateVersion v5 = new UpdateVersion(1, 1, 1, VersionLabel.Alpha); + UpdateVersion v6 = new UpdateVersion(1, 1, 1, VersionLabel.Beta); + UpdateVersion v7 = new UpdateVersion(1, 1, 1, VersionLabel.RC); + + Assert.True(v1 == v2, "v1 == v2"); + Assert.True(v1 != v3, "v1 != v3"); + + Assert.True(v3 > v1, "v3 > v1"); + Assert.False(v4 < v3, "v4 < v3"); + + Assert.True(v7 > v6, "v7 > v6"); + Assert.True(v6 > v5, "v6 > v5"); + } + + [Fact] + public void TestConversion() + { + string input = "1.1.1-rc"; + + UpdateVersion v = input; + + Assert.Equal(1, v.Major); + Assert.Equal(1, v.Minor); + Assert.Equal(1, v.Patch); + Assert.Equal(VersionLabel.RC, v.Label); + + string output = v; + + Assert.Equal(input, output); + } + } +} diff --git a/UpdateLib.Tests/UpdateLib.Tests.csproj b/UpdateLib.Tests/UpdateLib.Tests.csproj index 3e02510..3a80047 100644 --- a/UpdateLib.Tests/UpdateLib.Tests.csproj +++ b/UpdateLib.Tests/UpdateLib.Tests.csproj @@ -12,4 +12,8 @@ + + + + diff --git a/UpdateLib/Abstractions/IUpdater.cs b/UpdateLib/Abstractions/IUpdater.cs index 29e28d3..02e395e 100644 --- a/UpdateLib/Abstractions/IUpdater.cs +++ b/UpdateLib/Abstractions/IUpdater.cs @@ -1,10 +1,12 @@ -using System; -using System.Collections.Generic; -using System.Text; + +using System.Threading.Tasks; +using UpdateLib.Core; namespace UpdateLib.Abstractions { interface IUpdater { + Task CheckForUpdatesAsync(); + CheckForUpdatesResult CheckForUpdates(); } } diff --git a/UpdateLib/Core/CheckForUpdatesResult.cs b/UpdateLib/Core/CheckForUpdatesResult.cs new file mode 100644 index 0000000..ce2174a --- /dev/null +++ b/UpdateLib/Core/CheckForUpdatesResult.cs @@ -0,0 +1,10 @@ +namespace UpdateLib.Core +{ + public class CheckForUpdatesResult + { + public bool UpdateAvailable { get; private set; } + + public UpdateVersion CurrentVersion { get; private set; } + public UpdateVersion NewVersion { get; private set; } + } +} diff --git a/UpdateLib/Core/Enums/VersionLabel.cs b/UpdateLib/Core/Enums/VersionLabel.cs new file mode 100644 index 0000000..86ef1fb --- /dev/null +++ b/UpdateLib/Core/Enums/VersionLabel.cs @@ -0,0 +1,10 @@ +namespace UpdateLib.Core.Enums +{ + public enum VersionLabel : byte + { + Alpha = 0, + Beta = 1, + RC = 2, + None = 3 + } +} diff --git a/UpdateLib/Core/UpdateVersion.cs b/UpdateLib/Core/UpdateVersion.cs new file mode 100644 index 0000000..dd5bbbb --- /dev/null +++ b/UpdateLib/Core/UpdateVersion.cs @@ -0,0 +1,275 @@ +using System; +using System.Diagnostics; +using System.Text.RegularExpressions; +using UpdateLib.Core.Enums; + +namespace UpdateLib.Core +{ + /// + /// Versioning class with small extensions over the original as the original is sealed. + /// Support for version label's and serializable. + /// Partially based on Semantic Versioning + /// + [DebuggerDisplay("{Value}")] + public class UpdateVersion : IComparable, IComparable, IEquatable + { + private int m_major, m_minor, m_patch; + private VersionLabel m_label; + + #region Constants + + private const string ALPHA_STRING = "-alpha"; + private const string BETA_STRING = "-beta"; + private const string RC_STRING = "-rc"; + private static readonly char[] CharSplitDot = new char[] { '.' }; + private static readonly char[] CharSplitDash = new char[] { '-' }; + private static readonly Regex m_regex = new Regex(@"([v]?[0-9]+){1}(\.[0-9]+){0,2}([-](alpha|beta|rc))?"); + + #endregion + + #region Properties + + public int Major => m_major; + + public int Minor => m_minor; + + public int Patch => m_patch; + + public VersionLabel Label => m_label; + + public string Value + { + get { return ToString(); } + set + { + UpdateVersion version; + + if (!TryParse(value, out version)) + throw new ArgumentException(nameof(value), "Unable to parse input string"); + + m_major = version.m_major; + m_minor = version.m_minor; + m_patch = version.m_patch; + m_label = version.m_label; + } + } + + #endregion + + #region Constructor + + public UpdateVersion() + : this(0, 0, 0, VersionLabel.None) + { } + + public UpdateVersion(int major) + : this(major, 0, 0, VersionLabel.None) + { } + + public UpdateVersion(int major, int minor) + : this(major, minor, 0, VersionLabel.None) + { } + + public UpdateVersion(int major, int minor, int patch) + : this(major, minor, patch, VersionLabel.None) + { } + + public UpdateVersion(int major, int minor, int patch, VersionLabel label) + { + if (major < 0) throw new ArgumentOutOfRangeException(nameof(major), "Version cannot be negative"); + if (minor < 0) throw new ArgumentOutOfRangeException(nameof(minor), "Version cannot be negative"); + if (patch < 0) throw new ArgumentOutOfRangeException(nameof(patch), "Version cannot be negative"); + + m_major = major; + m_minor = minor; + m_patch = patch; + m_label = label; + } + + public UpdateVersion(string input) + { + if (!TryParse(input, out UpdateVersion version)) + throw new ArgumentException(nameof(input), "Unable to parse input string"); + + m_major = version.m_major; + m_minor = version.m_minor; + m_patch = version.m_patch; + m_label = version.m_label; + } + + #endregion + + #region Interface Impl. + + public int CompareTo(UpdateVersion other) + { + if (other == null) + return 1; + + if (m_major != other.m_major) + return m_major > other.m_major ? 1 : -1; + + if (m_minor != other.m_minor) + return m_minor > other.m_minor ? 1 : -1; + + if (m_patch != other.m_patch) + return m_patch > other.m_patch ? 1 : -1; + + if (m_label != other.m_label) + return m_label > other.m_label ? 1 : -1; + + return 0; + } + + public int CompareTo(object obj) + { + UpdateVersion other = obj as UpdateVersion; + + if (other == null) + return 1; + + return CompareTo(other); + } + + public bool Equals(UpdateVersion other) + { + if (other == null) + return false; + + return m_major == other.m_major + && m_minor == other.m_minor + && m_patch == other.m_patch + && m_label == other.m_label; + } + + public override bool Equals(object obj) + => Equals(obj as UpdateVersion); + + public override int GetHashCode() + { + int hash = 269; + + hash = (hash * 47) + Major.GetHashCode(); + hash = (hash * 47) + Minor.GetHashCode(); + hash = (hash * 47) + Patch.GetHashCode(); + hash = (hash * 47) + Label.GetHashCode(); + + return hash; + } + + #endregion + + public override string ToString() => $"{m_major}.{m_minor}.{m_patch}{LabelToString()}"; + + private string LabelToString() + { + switch (m_label) + { + case VersionLabel.Alpha: + return ALPHA_STRING; + case VersionLabel.Beta: + return BETA_STRING; + case VersionLabel.RC: + return RC_STRING; + case VersionLabel.None: + default: + return string.Empty; + } + } + + private static bool TryParseVersionLabelString(string input, out VersionLabel label) + { + if (input == string.Empty) + { + label = VersionLabel.None; + return true; + } + + input = $"-{input}"; + + if (input == ALPHA_STRING) + { + label = VersionLabel.Alpha; + return true; + } + else if (input == BETA_STRING) + { + label = VersionLabel.Beta; + return true; + } + else if (input == RC_STRING) + { + label = VersionLabel.RC; + return true; + } + else + { + label = VersionLabel.None; + return false; + } + } + + public static bool CanParse(string input) + => m_regex.IsMatch(input); + + /// + /// Tries to parse the to a + /// + /// Input string should be of format "(v)major.minor.patch(-label)". The (v) and (-label) are optional + /// The output parameter + /// True if succesfully parsed, false if failed + public static bool TryParse(string input, out UpdateVersion version) + { + version = new UpdateVersion(); + + if (!CanParse(input)) return false; + + if (input.StartsWith("v")) + input = input.Substring(1, input.Length - 2); + + var dashSplitTokens = input.Split(CharSplitDash); + var tokens = dashSplitTokens[0].Split(CharSplitDot); + + if (tokens.Length > 3 || dashSplitTokens.Length > 2) // invalid version format, needs to be the following major.minor.patch(-label) + return false; + + if (tokens.Length > 2 && !int.TryParse(tokens[2], out version.m_patch)) + return false; + + if (dashSplitTokens.Length > 1 && !TryParseVersionLabelString(dashSplitTokens[1], out version.m_label)) // unable to parse the version label + return false; + + if (tokens.Length > 1 && !int.TryParse(tokens[1], out version.m_minor)) + return false; + + if (tokens.Length > 0 && !int.TryParse(tokens[0], out version.m_major)) + return false; + + return true; // everything parsed succesfully + } + + public static bool operator ==(UpdateVersion v1, UpdateVersion v2) + => ReferenceEquals(v1, null) ? ReferenceEquals(v2, null) : v1.Equals(v2); + + public static bool operator !=(UpdateVersion v1, UpdateVersion v2) + => !(v1 == v2); + + public static bool operator >(UpdateVersion v1, UpdateVersion v2) + => v2 < v1; + + public static bool operator >=(UpdateVersion v1, UpdateVersion v2) + => v2 <= v1; + + public static bool operator <(UpdateVersion v1, UpdateVersion v2) + => !ReferenceEquals(v1, null) && v1.CompareTo(v2) < 0; + + public static bool operator <=(UpdateVersion v1, UpdateVersion v2) + => !ReferenceEquals(v1, null) && v1.CompareTo(v2) <= 0; + + public static implicit operator UpdateVersion(string value) + => new UpdateVersion(value); + + public static implicit operator string(UpdateVersion version) + => version.Value; + } +} diff --git a/UpdateLib/UpdateLib.csproj b/UpdateLib/UpdateLib.csproj index dd7c19d..44d41f6 100644 --- a/UpdateLib/UpdateLib.csproj +++ b/UpdateLib/UpdateLib.csproj @@ -4,10 +4,6 @@ netstandard2.0 - - - - diff --git a/UpdateLib/Updater.cs b/UpdateLib/Updater.cs index deb6eab..83ba5b5 100644 --- a/UpdateLib/Updater.cs +++ b/UpdateLib/Updater.cs @@ -1,8 +1,19 @@ -using UpdateLib.Abstractions; +using System.Threading.Tasks; +using UpdateLib.Abstractions; +using UpdateLib.Core; namespace UpdateLib { public class Updater : IUpdater { + public CheckForUpdatesResult CheckForUpdates() + { + throw new System.NotImplementedException(); + } + + public Task CheckForUpdatesAsync() + { + throw new System.NotImplementedException(); + } } } From 21f3579548dc858da027ffac3656e842fecbea53 Mon Sep 17 00:00:00 2001 From: Matthias Beerens <3512339+Matthiee@users.noreply.github.com> Date: Wed, 25 Dec 2019 09:00:02 +0100 Subject: [PATCH 02/14] Add Cache Manager - Save, Update, Load cache --- UpdateLib.Tests/UpdateLib.Tests.csproj | 2 + UpdateLib/Abstractions/ICacheManager.cs | 9 ++ UpdateLib/Abstractions/IUpdater.cs | 2 +- .../Abstractions/Storage/ICacheStorage.cs | 12 ++ UpdateLib/Core/CacheManager.cs | 136 ++++++++++++++++++ UpdateLib/Core/CheckForUpdatesResult.cs | 2 - UpdateLib/Core/Extensions.cs | 43 ++++++ UpdateLib/Core/Storage/CacheStorage.cs | 56 ++++++++ .../Core/Storage/Files/HashCacheEntry.cs | 16 +++ UpdateLib/Core/Storage/Files/HashCacheFile.cs | 10 ++ UpdateLib/UpdateLib.csproj | 3 + UpdateLib/Updater.cs | 12 +- 12 files changed, 298 insertions(+), 5 deletions(-) create mode 100644 UpdateLib/Abstractions/ICacheManager.cs create mode 100644 UpdateLib/Abstractions/Storage/ICacheStorage.cs create mode 100644 UpdateLib/Core/CacheManager.cs create mode 100644 UpdateLib/Core/Extensions.cs create mode 100644 UpdateLib/Core/Storage/CacheStorage.cs create mode 100644 UpdateLib/Core/Storage/Files/HashCacheEntry.cs create mode 100644 UpdateLib/Core/Storage/Files/HashCacheFile.cs diff --git a/UpdateLib.Tests/UpdateLib.Tests.csproj b/UpdateLib.Tests/UpdateLib.Tests.csproj index 3a80047..1a44a07 100644 --- a/UpdateLib.Tests/UpdateLib.Tests.csproj +++ b/UpdateLib.Tests/UpdateLib.Tests.csproj @@ -8,6 +8,8 @@ + + diff --git a/UpdateLib/Abstractions/ICacheManager.cs b/UpdateLib/Abstractions/ICacheManager.cs new file mode 100644 index 0000000..c53b835 --- /dev/null +++ b/UpdateLib/Abstractions/ICacheManager.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace UpdateLib.Abstractions +{ + interface ICacheManager + { + Task UpdateCacheAsync(); + } +} diff --git a/UpdateLib/Abstractions/IUpdater.cs b/UpdateLib/Abstractions/IUpdater.cs index 02e395e..136e635 100644 --- a/UpdateLib/Abstractions/IUpdater.cs +++ b/UpdateLib/Abstractions/IUpdater.cs @@ -7,6 +7,6 @@ namespace UpdateLib.Abstractions interface IUpdater { Task CheckForUpdatesAsync(); - CheckForUpdatesResult CheckForUpdates(); + Task InitializeAsync(); } } diff --git a/UpdateLib/Abstractions/Storage/ICacheStorage.cs b/UpdateLib/Abstractions/Storage/ICacheStorage.cs new file mode 100644 index 0000000..f3f956f --- /dev/null +++ b/UpdateLib/Abstractions/Storage/ICacheStorage.cs @@ -0,0 +1,12 @@ +using System; +using System.Threading.Tasks; +using UpdateLib.Core.Storage.Files; + +namespace UpdateLib.Abstractions.Storage +{ + public interface ICacheStorage + { + Task SaveAsync(HashCacheFile file); + Task LoadAsync(); + } +} diff --git a/UpdateLib/Core/CacheManager.cs b/UpdateLib/Core/CacheManager.cs new file mode 100644 index 0000000..af00f81 --- /dev/null +++ b/UpdateLib/Core/CacheManager.cs @@ -0,0 +1,136 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Abstractions; +using System.Linq; +using System.Security.Cryptography; +using System.Threading.Tasks; +using UpdateLib.Abstractions; +using UpdateLib.Abstractions.Storage; +using UpdateLib.Core.Storage.Files; + +namespace UpdateLib.Core +{ + public class CacheManager : ICacheManager + { + private readonly IFileSystem fs; + private readonly ICacheStorage cacheStorage; + private readonly ILogger logger; + private IEnumerable files; + + public CacheManager(IFileSystem fs, ICacheStorage cacheStorage, ILogger logger) + { + this.fs = fs ?? throw new ArgumentNullException(nameof(fs)); + this.cacheStorage = cacheStorage ?? throw new ArgumentNullException(nameof(cacheStorage)); + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public async Task UpdateCacheAsync() + { + HashCacheFile file = null; + + try + { + file = await cacheStorage.LoadAsync(); + } + catch (Exception e) + { + logger.LogError(e, "Unable to load cache from storage"); + } + + files = fs.DirectoryInfo.FromDirectoryName(".").GetFiles("*", SearchOption.AllDirectories).Where(f => !f.FullName.Contains(".old.tmp")); + + logger.LogDebug($"Found {files.Count()} to recheck."); + + if (file == null) + { + file = await CreateNewHashCacheFileAsync(); + return; + } + + await UpdateExistingFiles(file); + + await cacheStorage.SaveAsync(file); + } + + private async Task UpdateExistingFiles(HashCacheFile cacheFile) + { + Dictionary existingEntries = new Dictionary(cacheFile.Entries.Count); + + foreach (var entry in cacheFile.Entries) + { + existingEntries.Add(entry, false); + } + + foreach (var file in files) + { + var entry = cacheFile.Entries.FirstOrDefault(match => match.FilePath == file.FullName); + + HashCacheEntry newEntry = null; + + try + { + newEntry = await CreateNewEntry(file).ConfigureAwait(false); + } + catch (Exception ex) + { + logger.LogError(ex, $"Unable to create cache entry for {file.FullName}. The file might be in user or no longer exists."); + } + + if (newEntry == null) + continue; + + if (entry != null) + { + existingEntries[entry] = true; + + entry.Hash = newEntry.Hash; + entry.Ticks = newEntry.Ticks; + } + else + { + cacheFile.Entries.Add(newEntry); + } + } + + existingEntries.Where(item => !item.Value).Select(item => item.Key).ForEach(entry => cacheFile.Entries.Remove(entry)); + } + + private async Task CreateNewHashCacheFileAsync() + { + var result = new HashCacheFile(); + + foreach (var f in files) + { + try + { + var entry = await CreateNewEntry(f).ConfigureAwait(false); + + result.Entries.Add(entry); + + } + catch (Exception e) + { + logger.LogError(e, $"Unable to create cache entry for {f.FullName}. The file might be in user or no longer exists."); + } + } + + await cacheStorage.SaveAsync(result); + + return result; + } + + private async Task CreateNewEntry(IFileInfo fileInfo) + { + using (var stream = fileInfo.OpenRead()) + { + var hash = await stream.GetHashAsync(); + var ticks = fileInfo.LastWriteTimeUtc.Ticks; + var name = fileInfo.FullName; + + return new HashCacheEntry(name, ticks, hash); + } + } + } +} diff --git a/UpdateLib/Core/CheckForUpdatesResult.cs b/UpdateLib/Core/CheckForUpdatesResult.cs index ce2174a..a7ca87a 100644 --- a/UpdateLib/Core/CheckForUpdatesResult.cs +++ b/UpdateLib/Core/CheckForUpdatesResult.cs @@ -3,8 +3,6 @@ public class CheckForUpdatesResult { public bool UpdateAvailable { get; private set; } - - public UpdateVersion CurrentVersion { get; private set; } public UpdateVersion NewVersion { get; private set; } } } diff --git a/UpdateLib/Core/Extensions.cs b/UpdateLib/Core/Extensions.cs new file mode 100644 index 0000000..32b2b13 --- /dev/null +++ b/UpdateLib/Core/Extensions.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; + +namespace UpdateLib.Core +{ + public static class Extensions + { + public static async Task GetHashAsync(this Stream stream) + where T : HashAlgorithm, new() + { + StringBuilder sb; + + using (var algo = new T()) + { + var buffer = new byte[8192]; + int bytesRead; + + // compute the hash on 8KiB blocks + while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length)) != 0) + algo.TransformBlock(buffer, 0, bytesRead, buffer, 0); + + algo.TransformFinalBlock(buffer, 0, bytesRead); + + // build the hash string + sb = new StringBuilder(algo.HashSize / 4); + foreach (var b in algo.Hash) + sb.AppendFormat("{0:x2}", b); + } + + return sb?.ToString(); + } + + public static void ForEach(this IEnumerable collection, Action action) + { + foreach (var item in collection) + action(item); + } + } +} diff --git a/UpdateLib/Core/Storage/CacheStorage.cs b/UpdateLib/Core/Storage/CacheStorage.cs new file mode 100644 index 0000000..fb30934 --- /dev/null +++ b/UpdateLib/Core/Storage/CacheStorage.cs @@ -0,0 +1,56 @@ +using Newtonsoft.Json; +using System; +using System.IO; +using System.IO.Abstractions; +using System.Threading.Tasks; +using UpdateLib.Abstractions.Storage; +using UpdateLib.Core.Storage.Files; +using static System.Environment; + +namespace UpdateLib.Core.Storage +{ + public class CacheStorage : ICacheStorage + { + private const string CachePathName = "Cache"; + private const string CacheFileName = "FileCache.json"; + + private readonly IFileSystem fs; + private readonly string cachePath; + + public CacheStorage(IFileSystem storage) + { + this.fs = storage ?? throw new ArgumentNullException(nameof(storage)); + + cachePath = GetFilePathAndEnsureCreated(); + } + + private string GetFilePathAndEnsureCreated() + { + // Use DoNotVerify in case LocalApplicationData doesn’t exist. + string path = fs.Path.Combine(GetFolderPath(SpecialFolder.LocalApplicationData, SpecialFolderOption.DoNotVerify), "UpdateLib", CachePathName, CacheFileName); + // Ensure the directory and all its parents exist. + fs.Directory.CreateDirectory(path); + + return path; + } + + public async Task LoadAsync() + { + using (var reader = fs.File.OpenText(cachePath)) + { + var contents = await reader.ReadToEndAsync(); + return JsonConvert.DeserializeObject(contents); + } + } + + public async Task SaveAsync(HashCacheFile file) + { + using (var stream = fs.File.OpenWrite(cachePath)) + using (var writer = new StreamWriter(stream)) + { + var contents = JsonConvert.SerializeObject(file); + await writer.WriteAsync(contents); + } + } + } +} diff --git a/UpdateLib/Core/Storage/Files/HashCacheEntry.cs b/UpdateLib/Core/Storage/Files/HashCacheEntry.cs new file mode 100644 index 0000000..81bc2a9 --- /dev/null +++ b/UpdateLib/Core/Storage/Files/HashCacheEntry.cs @@ -0,0 +1,16 @@ +namespace UpdateLib.Core.Storage.Files +{ + public class HashCacheEntry + { + public HashCacheEntry(string name, long ticks, string hash) + { + FilePath = name; + Ticks = ticks; + Hash = hash; + } + + public long Ticks { get; set; } + public string FilePath { get; set; } + public string Hash { get; set; } + } +} diff --git a/UpdateLib/Core/Storage/Files/HashCacheFile.cs b/UpdateLib/Core/Storage/Files/HashCacheFile.cs new file mode 100644 index 0000000..4c60d40 --- /dev/null +++ b/UpdateLib/Core/Storage/Files/HashCacheFile.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; + +namespace UpdateLib.Core.Storage.Files +{ + public class HashCacheFile + { + public List Entries { get; set; } + } +} diff --git a/UpdateLib/UpdateLib.csproj b/UpdateLib/UpdateLib.csproj index 44d41f6..ff3c508 100644 --- a/UpdateLib/UpdateLib.csproj +++ b/UpdateLib/UpdateLib.csproj @@ -6,6 +6,9 @@ + + + diff --git a/UpdateLib/Updater.cs b/UpdateLib/Updater.cs index 83ba5b5..a138e5f 100644 --- a/UpdateLib/Updater.cs +++ b/UpdateLib/Updater.cs @@ -1,19 +1,27 @@ using System.Threading.Tasks; using UpdateLib.Abstractions; +using UpdateLib.Abstractions.Storage; using UpdateLib.Core; namespace UpdateLib { public class Updater : IUpdater { - public CheckForUpdatesResult CheckForUpdates() + private readonly ICacheStorage cacheStorage; + + public Updater(ICacheStorage cacheStorage) { - throw new System.NotImplementedException(); + this.cacheStorage = cacheStorage ?? throw new System.ArgumentNullException(nameof(cacheStorage)); } public Task CheckForUpdatesAsync() { throw new System.NotImplementedException(); } + + public async Task InitializeAsync() + { + var cache = await cacheStorage.LoadAsync(); + } } } From 0f55bc5fd3c9e37b212ede74ce8bf9245059e29a Mon Sep 17 00:00:00 2001 From: Matthias Beerens <3512339+Matthiee@users.noreply.github.com> Date: Fri, 27 Dec 2019 12:43:49 +0100 Subject: [PATCH 03/14] Add DirectoryEntry and FIleEntry --- .../Core/Common/IO/FileEntryTests.cs | 26 +++++ .../{Common => Core}/UpdateVersionTests.cs | 2 +- UpdateLib.Tests/UpdateLib.Tests.csproj | 2 +- UpdateLib/Core/Common/IO/DirectoryEntry.cs | 107 ++++++++++++++++++ UpdateLib/Core/Common/IO/FileEntry.cs | 55 +++++++++ UpdateLib/UpdateLib.csproj | 2 +- 6 files changed, 191 insertions(+), 3 deletions(-) create mode 100644 UpdateLib.Tests/Core/Common/IO/FileEntryTests.cs rename UpdateLib.Tests/{Common => Core}/UpdateVersionTests.cs (99%) create mode 100644 UpdateLib/Core/Common/IO/DirectoryEntry.cs create mode 100644 UpdateLib/Core/Common/IO/FileEntry.cs diff --git a/UpdateLib.Tests/Core/Common/IO/FileEntryTests.cs b/UpdateLib.Tests/Core/Common/IO/FileEntryTests.cs new file mode 100644 index 0000000..34255b3 --- /dev/null +++ b/UpdateLib.Tests/Core/Common/IO/FileEntryTests.cs @@ -0,0 +1,26 @@ +using Xunit; +using UpdateLib.Core.Common.IO; + +namespace UpdateLib.Tests.Core.Common.IO +{ + public class FileEntryTests + { + [Fact] + public void ShouldGiveCorrectSourceAndDestination() + { + DirectoryEntry root = new DirectoryEntry("%root%"); + DirectoryEntry subFolder = new DirectoryEntry("sub"); + FileEntry file = new FileEntry("myfile.txt"); + + root.Add(subFolder); + + subFolder.Add(file); + + string outputSource = "sub/myfile.txt"; + string outputDest = "%root%\\sub\\myfile.txt"; + + Assert.Equal(outputSource, file.SourceLocation); + Assert.Equal(outputDest, file.DestinationLocation); + } + } +} diff --git a/UpdateLib.Tests/Common/UpdateVersionTests.cs b/UpdateLib.Tests/Core/UpdateVersionTests.cs similarity index 99% rename from UpdateLib.Tests/Common/UpdateVersionTests.cs rename to UpdateLib.Tests/Core/UpdateVersionTests.cs index c45a539..4ebbaaf 100644 --- a/UpdateLib.Tests/Common/UpdateVersionTests.cs +++ b/UpdateLib.Tests/Core/UpdateVersionTests.cs @@ -3,7 +3,7 @@ using UpdateLib.Core.Enums; using Xunit; -namespace UpdateLib.Tests.Common +namespace UpdateLib.Tests.Core { public class UpdateVersionTests { diff --git a/UpdateLib.Tests/UpdateLib.Tests.csproj b/UpdateLib.Tests/UpdateLib.Tests.csproj index 1a44a07..eb5e6d0 100644 --- a/UpdateLib.Tests/UpdateLib.Tests.csproj +++ b/UpdateLib.Tests/UpdateLib.Tests.csproj @@ -1,4 +1,4 @@ - + netcoreapp2.2 diff --git a/UpdateLib/Core/Common/IO/DirectoryEntry.cs b/UpdateLib/Core/Common/IO/DirectoryEntry.cs new file mode 100644 index 0000000..1a66987 --- /dev/null +++ b/UpdateLib/Core/Common/IO/DirectoryEntry.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace UpdateLib.Core.Common.IO +{ + public class DirectoryEntry + { + private readonly List directories = new List(); + private readonly List files = new List(); + + public string Name { get; set; } + + public int Count => Files.Count + Directories.Sum(d => d.Count); + public IReadOnlyList Directories => directories.AsReadOnly(); + public IReadOnlyList Files => files.AsReadOnly(); + public DirectoryEntry Parent { get; set; } + + public string SourceLocation + { + get + { + StringBuilder sb = new StringBuilder(); + + if (Parent == null) + return string.Empty; + + sb.Append(Parent.SourceLocation); + sb.Append(Name); + sb.Append(@"/"); + + return sb.ToString(); + } + } + + public string DestinationLocation + { + get + { + StringBuilder sb = new StringBuilder(); + + sb.Append(Parent?.DestinationLocation ?? string.Empty); + sb.Append(Name); + sb.Append(@"\"); + + return sb.ToString(); + } + } + + public DirectoryEntry() + { + } + + public DirectoryEntry(string name) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + } + + public DirectoryEntry(string name, DirectoryEntry parent) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + Parent = parent ?? throw new ArgumentNullException(nameof(parent)); + } + + public void Add(DirectoryEntry folder) + { + if (folder == null) throw new ArgumentNullException(nameof(folder)); + + folder.Parent = this; + directories.Add(folder); + } + + public void Add(FileEntry file) + { + if (file == null) throw new ArgumentNullException(nameof(file)); + + file.Parent = this; + files.Add(file); + } + + public bool Remove(DirectoryEntry folder) + { + if (folder == null) throw new ArgumentNullException(nameof(folder)); + + folder.Parent = null; + return directories.Remove(folder); + } + + public bool Remove(FileEntry file) + { + if (file == null) throw new ArgumentNullException(nameof(file)); + + file.Parent = null; + return files.Remove(file); + } + + /// + /// Gets all the items including the items of childs + /// + /// A list of items + public IEnumerable GetItems() + { + return Files.Concat(Directories.SelectMany(d => d.GetItems())); + } + } +} diff --git a/UpdateLib/Core/Common/IO/FileEntry.cs b/UpdateLib/Core/Common/IO/FileEntry.cs new file mode 100644 index 0000000..309eef8 --- /dev/null +++ b/UpdateLib/Core/Common/IO/FileEntry.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace UpdateLib.Core.Common.IO +{ + public class FileEntry + { + public string Hash { get; set; } + + public string Name { get; set; } + + public DirectoryEntry Parent { get; set; } + + public FileEntry() { } + + public FileEntry(string name) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + } + + public FileEntry(string name, DirectoryEntry parent) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + Parent = parent ?? throw new ArgumentNullException(nameof(parent)); + } + + public string SourceLocation + { + get + { + StringBuilder sb = new StringBuilder(); + + sb.Append(Parent?.SourceLocation ?? string.Empty); + sb.Append(Name); + + return sb.ToString(); + } + } + + public string DestinationLocation + { + get + { + StringBuilder sb = new StringBuilder(); + + sb.Append(Parent?.DestinationLocation ?? string.Empty); + sb.Append(Name); + + return sb.ToString(); + } + } + } +} diff --git a/UpdateLib/UpdateLib.csproj b/UpdateLib/UpdateLib.csproj index ff3c508..f996516 100644 --- a/UpdateLib/UpdateLib.csproj +++ b/UpdateLib/UpdateLib.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 From e2d14e5e3757b909b4f0f226a36c0d9841afb9b7 Mon Sep 17 00:00:00 2001 From: Matthias Beerens <3512339+Matthiee@users.noreply.github.com> Date: Sat, 28 Dec 2019 15:38:13 +0100 Subject: [PATCH 04/14] Add updateinfo --- .../Core/Common/UpdateInfoTests.cs | 23 ++++++++ UpdateLib.Tests/UnitTest1.cs | 14 ----- UpdateLib/Core/Common/UpdateInfo.cs | 58 +++++++++++++++++++ 3 files changed, 81 insertions(+), 14 deletions(-) create mode 100644 UpdateLib.Tests/Core/Common/UpdateInfoTests.cs delete mode 100644 UpdateLib.Tests/UnitTest1.cs create mode 100644 UpdateLib/Core/Common/UpdateInfo.cs diff --git a/UpdateLib.Tests/Core/Common/UpdateInfoTests.cs b/UpdateLib.Tests/Core/Common/UpdateInfoTests.cs new file mode 100644 index 0000000..f4ef114 --- /dev/null +++ b/UpdateLib.Tests/Core/Common/UpdateInfoTests.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Text; +using UpdateLib.Core; +using UpdateLib.Core.Common; +using Xunit; + +namespace UpdateLib.Tests.Core.Common +{ + public class UpdateInfoTests + { + [Theory] + [InlineData("1.0.0", "2.0.0")] + [InlineData("1.0.0", "1.0.0")] + public void BasedOnVersionHigherThenSelfVersionThrowsException(string currVersion, string baseVersion) + { + var basedOnVersion = new UpdateVersion(baseVersion); + var version = new UpdateVersion(currVersion); + + Assert.Throws(() => new UpdateInfo(version, basedOnVersion, "", "")); + } + } +} diff --git a/UpdateLib.Tests/UnitTest1.cs b/UpdateLib.Tests/UnitTest1.cs deleted file mode 100644 index 9c2effe..0000000 --- a/UpdateLib.Tests/UnitTest1.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using Xunit; - -namespace UpdateLib.Tests -{ - public class UnitTest1 - { - [Fact] - public void Test1() - { - - } - } -} diff --git a/UpdateLib/Core/Common/UpdateInfo.cs b/UpdateLib/Core/Common/UpdateInfo.cs new file mode 100644 index 0000000..7c072f8 --- /dev/null +++ b/UpdateLib/Core/Common/UpdateInfo.cs @@ -0,0 +1,58 @@ +using System; +using Newtonsoft.Json; + +namespace UpdateLib.Core.Common +{ + public class UpdateInfo : IComparable, IComparable + { + public UpdateVersion BasedOnVersion { get; set; } + public UpdateVersion Version { get; set; } + public string FileName { get; set; } + public string Hash { get; set; } + + [JsonIgnore] + public bool IsPatch => BasedOnVersion != null; + + public UpdateInfo() { } + + /// + /// A new catalog entry + /// + /// The update version + /// The version this update is based on, can be null if it's not a patch. + /// The file name for the update. + /// The calculated hash for the update + public UpdateInfo(UpdateVersion version, UpdateVersion basedOnVersion, string fileName, string hash) + { + Version = version ?? throw new ArgumentNullException(nameof(version)); + FileName = fileName ?? throw new ArgumentNullException(nameof(fileName)); + Hash = hash ?? throw new ArgumentNullException(nameof(hash)); + + BasedOnVersion = basedOnVersion; + + if (version <= basedOnVersion) throw new ArgumentOutOfRangeException(nameof(basedOnVersion), "The new version cannot be smaller than the version it was based on."); + } + + public int CompareTo(UpdateInfo other) + { + if (other == null) return -1; + + if (Version > other.Version) return -1; + + if (Version == other.Version) + { + if (IsPatch && other.IsPatch) return BasedOnVersion.CompareTo(other.BasedOnVersion); + + if (IsPatch && !other.IsPatch) return -1; + + if (!IsPatch && other.IsPatch) return 1; + + return 0; + } + + return 1; + } + + public int CompareTo(object obj) => CompareTo(obj as UpdateInfo); + } +} From 2b29c673dd5f65b364d2862c0df2ad79b104b079 Mon Sep 17 00:00:00 2001 From: Matthias Beerens <3512339+Matthiee@users.noreply.github.com> Date: Sat, 28 Dec 2019 16:23:21 +0100 Subject: [PATCH 05/14] Add Cache Storage Tests --- .../Core/Storage/CacheStorageTests.cs | 38 +++++++++++++++++++ UpdateLib/Core/Storage/Files/HashCacheFile.cs | 5 +++ 2 files changed, 43 insertions(+) create mode 100644 UpdateLib.Tests/Core/Storage/CacheStorageTests.cs diff --git a/UpdateLib.Tests/Core/Storage/CacheStorageTests.cs b/UpdateLib.Tests/Core/Storage/CacheStorageTests.cs new file mode 100644 index 0000000..c7e4f70 --- /dev/null +++ b/UpdateLib.Tests/Core/Storage/CacheStorageTests.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.IO.Abstractions.TestingHelpers; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UpdateLib.Core.Storage; +using UpdateLib.Core.Storage.Files; +using Xunit; +using static System.Environment; + +namespace UpdateLib.Tests.Core.Storage +{ + public class CacheStorageTests + { + [Fact] + public async Task CacheSaveAndLoadAreTheSame() + { + var mockFileSystem = new MockFileSystem(); + + var cache = new CacheStorage(mockFileSystem); + var file = new HashCacheFile(); + var entry = new HashCacheEntry("name", DateTime.UtcNow.Ticks, "some hash"); + + file.Entries.Add(entry); + + await cache.SaveAsync(file); + + var loadedFile = await cache.LoadAsync(); + + var loadedEntry = loadedFile.Entries.First(); + + Assert.Equal(entry.FilePath, loadedEntry.FilePath); + Assert.Equal(entry.Hash, loadedEntry.Hash); + Assert.Equal(entry.Ticks, loadedEntry.Ticks); + } + } +} diff --git a/UpdateLib/Core/Storage/Files/HashCacheFile.cs b/UpdateLib/Core/Storage/Files/HashCacheFile.cs index 4c60d40..7d6bcd4 100644 --- a/UpdateLib/Core/Storage/Files/HashCacheFile.cs +++ b/UpdateLib/Core/Storage/Files/HashCacheFile.cs @@ -6,5 +6,10 @@ namespace UpdateLib.Core.Storage.Files public class HashCacheFile { public List Entries { get; set; } + + public HashCacheFile() + { + Entries = new List(); + } } } From 64dd3f6ad3f7ba9a4bc2fad936bbafd4aeb9fb7d Mon Sep 17 00:00:00 2001 From: Matthias Beerens <3512339+Matthiee@users.noreply.github.com> Date: Sat, 28 Dec 2019 17:26:02 +0100 Subject: [PATCH 06/14] Add tests for CacheManager --- UpdateLib.Tests/Core/CacheManagerTests.cs | 103 ++++++++++++++++++++++ UpdateLib.Tests/Helpers.cs | 9 ++ UpdateLib.Tests/UpdateLib.Tests.csproj | 1 + UpdateLib/Core/Storage/CacheStorage.cs | 3 + 4 files changed, 116 insertions(+) create mode 100644 UpdateLib.Tests/Core/CacheManagerTests.cs create mode 100644 UpdateLib.Tests/Helpers.cs diff --git a/UpdateLib.Tests/Core/CacheManagerTests.cs b/UpdateLib.Tests/Core/CacheManagerTests.cs new file mode 100644 index 0000000..aad62c5 --- /dev/null +++ b/UpdateLib.Tests/Core/CacheManagerTests.cs @@ -0,0 +1,103 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Console; +using System; +using System.Collections.Generic; +using System.IO.Abstractions; +using System.IO.Abstractions.TestingHelpers; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UpdateLib.Abstractions.Storage; +using UpdateLib.Core; +using UpdateLib.Core.Storage; +using UpdateLib.Core.Storage.Files; +using Xunit; +using static System.Environment; +using static UpdateLib.Tests.Helpers; + +namespace UpdateLib.Tests.Core +{ + public class CacheManagerTests + { + [Fact] + public async Task NonExistingCacheCreatesANewOne() + { + var fs = new MockFileSystem(); + var cache = new CacheStorage(fs); + var manager = CreateCacheManager(fs, cache); + + await manager.UpdateCacheAsync(); + + Assert.NotNull(await cache.LoadAsync()); + } + + [Fact] + public async Task OldCacheEntriesAreDeleted() + { + var fs = new MockFileSystem(); + var cache = new CacheStorage(fs); + + var file = new HashCacheFile(); + file.Entries.Add(new HashCacheEntry("name", 0, "")); + + await cache.SaveAsync(file); + + var manager = CreateCacheManager(fs, cache); + + await manager.UpdateCacheAsync(); + + var result = await cache.LoadAsync(); + + Assert.Empty(result.Entries); + } + + [Fact] + public async Task UpdateCacheAddsNewEntries() + { + var fs = new MockFileSystem(); + fs.AddFile("./myfile.txt", new MockFileData("blabla")); + + var cache = new CacheStorage(fs); + var manager = CreateCacheManager(fs, cache); + + await manager.UpdateCacheAsync(); + + var result = (await cache.LoadAsync()).Entries.First(); + + Assert.Equal(fs.Path.GetFullPath("./myfile.txt"), result.FilePath); + } + + [Fact] + public async Task UpdateCacheAddsNewEntries_TempFilesAreIgnored() + { + var fs = new MockFileSystem(); + fs.AddFile("./myfile.txt", new MockFileData("blabla")); + fs.AddFile("./myfile.txt.old.tmp", new MockFileData("blabla")); + fs.AddFile("./someOtherFile.txt.old.tmp", new MockFileData("blabla")); + + var cache = new CacheStorage(fs); + var manager = CreateCacheManager(fs, cache); + + await manager.UpdateCacheAsync(); + + Assert.Single((await cache.LoadAsync()).Entries); + } + + [Fact] + public async Task CorruptCacheFileGetsRestored() + { + var fs = new MockFileSystem(); + string path = fs.Path.Combine(GetFolderPath(SpecialFolder.LocalApplicationData, SpecialFolderOption.DoNotVerify), "UpdateLib", "Cache", "FileCache.json"); + + fs.AddFile(path, new MockFileData("blabla")); // not valid json + + var cache = new CacheStorage(fs); + var manager = CreateCacheManager(fs, cache); + + await manager.UpdateCacheAsync(); + } + + private CacheManager CreateCacheManager(IFileSystem fs, ICacheStorage storage) + => new CacheManager(fs, storage, CreateLogger()); + } +} diff --git a/UpdateLib.Tests/Helpers.cs b/UpdateLib.Tests/Helpers.cs new file mode 100644 index 0000000..13c3cec --- /dev/null +++ b/UpdateLib.Tests/Helpers.cs @@ -0,0 +1,9 @@ +using Microsoft.Extensions.Logging; + +namespace UpdateLib.Tests +{ + public static class Helpers + { + public static ILogger CreateLogger() => LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger(); + } +} diff --git a/UpdateLib.Tests/UpdateLib.Tests.csproj b/UpdateLib.Tests/UpdateLib.Tests.csproj index eb5e6d0..6db7da9 100644 --- a/UpdateLib.Tests/UpdateLib.Tests.csproj +++ b/UpdateLib.Tests/UpdateLib.Tests.csproj @@ -7,6 +7,7 @@ + diff --git a/UpdateLib/Core/Storage/CacheStorage.cs b/UpdateLib/Core/Storage/CacheStorage.cs index fb30934..5cf306c 100644 --- a/UpdateLib/Core/Storage/CacheStorage.cs +++ b/UpdateLib/Core/Storage/CacheStorage.cs @@ -48,6 +48,9 @@ public async Task SaveAsync(HashCacheFile file) using (var stream = fs.File.OpenWrite(cachePath)) using (var writer = new StreamWriter(stream)) { + // truncate + stream.SetLength(0); + var contents = JsonConvert.SerializeObject(file); await writer.WriteAsync(contents); } From 95dcf29affb17b8b4513db537378536d6fdc8150 Mon Sep 17 00:00:00 2001 From: Matthias Beerens <3512339+Matthiee@users.noreply.github.com> Date: Sat, 28 Dec 2019 20:49:47 +0100 Subject: [PATCH 07/14] Add DirectoryEntry Tests --- .../Core/Common/IO/DirectoryEntryTests.cs | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 UpdateLib.Tests/Core/Common/IO/DirectoryEntryTests.cs diff --git a/UpdateLib.Tests/Core/Common/IO/DirectoryEntryTests.cs b/UpdateLib.Tests/Core/Common/IO/DirectoryEntryTests.cs new file mode 100644 index 0000000..f5e9f34 --- /dev/null +++ b/UpdateLib.Tests/Core/Common/IO/DirectoryEntryTests.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UpdateLib.Core.Common.IO; +using Xunit; + +namespace UpdateLib.Tests.Core.Common.IO +{ + public class DirectoryEntryTests + { + [Fact] + public void CountReturnsCorrectAmountOfFiles() + { + var root = CreateDirectoryWithFiles("1", 2); + var dir2 = CreateDirectoryWithFiles("2", 0); + var dir3 = CreateDirectoryWithFiles("3", 1); + var dir4 = CreateDirectoryWithFiles("4", 4); + var dir5 = CreateDirectoryWithFiles("5", 2); + + root.Add(dir2); + root.Add(dir3); + dir2.Add(dir4); + dir3.Add(dir5); + + Assert.Equal(9, root.Count); + Assert.Equal(9, root.GetItems().Count()); + } + + [Fact] + public void CountReturnsCorrectAmountOfFilesAfterDelete() + { + var root = CreateDirectoryWithFiles("1", 2); + var dir2 = CreateDirectoryWithFiles("2", 0); + var dir3 = CreateDirectoryWithFiles("3", 1); + var dir4 = CreateDirectoryWithFiles("4", 4); + var dir5 = CreateDirectoryWithFiles("5", 2); + + root.Add(dir2); + root.Add(dir3); + dir2.Add(dir4); + dir3.Add(dir5); + + root.Remove(dir2); + root.Remove(dir3); + + Assert.Equal(2, root.Count); + Assert.Equal(2, root.GetItems().Count()); + } + + [Fact] + public void RecursiveParentShouldEndUpWithRoot() + { + var root = CreateDirectoryWithFiles("1", 2); + var dir2 = CreateDirectoryWithFiles("2", 0); + var dir3 = CreateDirectoryWithFiles("3", 1); + var dir4 = CreateDirectoryWithFiles("4", 4); + var dir5 = CreateDirectoryWithFiles("5", 2); + + root.Add(dir2); + root.Add(dir3); + dir2.Add(dir4); + dir3.Add(dir5); + + var file = new FileEntry("custom"); + + dir5.Add(file); + + DirectoryEntry parent = file.Parent; + + while (parent.Parent != null) + { + parent = parent.Parent; + } + + Assert.Equal(root, parent); + } + + private DirectoryEntry CreateDirectoryWithFiles(string name, int filesToCreate) + { + var dir = new DirectoryEntry(name); + + for (int i = 0; i < filesToCreate; i++) + { + var entry = new FileEntry($"{name}.{i.ToString()}"); + + dir.Add(entry); + } + + return dir; + } + } +} From f1043806031cf4407866a3f87c533052a9bce6b5 Mon Sep 17 00:00:00 2001 From: Matthias Beerens <3512339+Matthiee@users.noreply.github.com> Date: Sun, 5 Jan 2020 20:53:04 +0100 Subject: [PATCH 08/14] WIP check for updates --- UpdateLib/Abstractions/ICacheManager.cs | 5 +- UpdateLib/Abstractions/IUpdater.cs | 1 + .../Storage/IUpdateFileStorage.cs | 11 ++++ UpdateLib/Core/CacheManager.cs | 6 +- UpdateLib/Core/Storage/Files/UpdateFile.cs | 14 +++++ UpdateLib/Core/Storage/UpdateFileStorage.cs | 58 +++++++++++++++++++ UpdateLib/UpdateLib.csproj | 1 + UpdateLib/Updater.cs | 26 ++++++--- 8 files changed, 111 insertions(+), 11 deletions(-) create mode 100644 UpdateLib/Abstractions/Storage/IUpdateFileStorage.cs create mode 100644 UpdateLib/Core/Storage/Files/UpdateFile.cs create mode 100644 UpdateLib/Core/Storage/UpdateFileStorage.cs diff --git a/UpdateLib/Abstractions/ICacheManager.cs b/UpdateLib/Abstractions/ICacheManager.cs index c53b835..dd90344 100644 --- a/UpdateLib/Abstractions/ICacheManager.cs +++ b/UpdateLib/Abstractions/ICacheManager.cs @@ -1,9 +1,10 @@ using System.Threading.Tasks; +using UpdateLib.Core.Storage.Files; namespace UpdateLib.Abstractions { - interface ICacheManager + public interface ICacheManager { - Task UpdateCacheAsync(); + Task UpdateCacheAsync(); } } diff --git a/UpdateLib/Abstractions/IUpdater.cs b/UpdateLib/Abstractions/IUpdater.cs index 136e635..4d61da6 100644 --- a/UpdateLib/Abstractions/IUpdater.cs +++ b/UpdateLib/Abstractions/IUpdater.cs @@ -6,6 +6,7 @@ namespace UpdateLib.Abstractions { interface IUpdater { + bool IsInitialized { get; } Task CheckForUpdatesAsync(); Task InitializeAsync(); } diff --git a/UpdateLib/Abstractions/Storage/IUpdateFileStorage.cs b/UpdateLib/Abstractions/Storage/IUpdateFileStorage.cs new file mode 100644 index 0000000..3c5f470 --- /dev/null +++ b/UpdateLib/Abstractions/Storage/IUpdateFileStorage.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using UpdateLib.Core.Storage.Files; + +namespace UpdateLib.Abstractions.Storage +{ + interface IUpdateFileStorage + { + Task SaveAsync(UpdateFile file); + Task LoadAsync(); + } +} diff --git a/UpdateLib/Core/CacheManager.cs b/UpdateLib/Core/CacheManager.cs index af00f81..d8b1399 100644 --- a/UpdateLib/Core/CacheManager.cs +++ b/UpdateLib/Core/CacheManager.cs @@ -26,7 +26,7 @@ public CacheManager(IFileSystem fs, ICacheStorage cacheStorage, ILogger UpdateCacheAsync() { HashCacheFile file = null; @@ -46,12 +46,14 @@ public async Task UpdateCacheAsync() if (file == null) { file = await CreateNewHashCacheFileAsync(); - return; + return file; } await UpdateExistingFiles(file); await cacheStorage.SaveAsync(file); + + return file; } private async Task UpdateExistingFiles(HashCacheFile cacheFile) diff --git a/UpdateLib/Core/Storage/Files/UpdateFile.cs b/UpdateLib/Core/Storage/Files/UpdateFile.cs new file mode 100644 index 0000000..25b31b0 --- /dev/null +++ b/UpdateLib/Core/Storage/Files/UpdateFile.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using UpdateLib.Core.Common.IO; + +namespace UpdateLib.Core.Storage.Files +{ + public class UpdateFile + { + public List Entries { get; set; } = new List(); + + public int FileCount => Entries.Sum(dir => dir.Count); + } +} diff --git a/UpdateLib/Core/Storage/UpdateFileStorage.cs b/UpdateLib/Core/Storage/UpdateFileStorage.cs new file mode 100644 index 0000000..8117ea8 --- /dev/null +++ b/UpdateLib/Core/Storage/UpdateFileStorage.cs @@ -0,0 +1,58 @@ +using Newtonsoft.Json; +using System; +using System.IO; +using System.IO.Abstractions; +using System.Threading.Tasks; +using UpdateLib.Abstractions.Storage; +using UpdateLib.Core.Storage.Files; +using static System.Environment; + +namespace UpdateLib.Core.Storage +{ + public class UpdateFileStorage : IUpdateFileStorage + { + private const string UpdateFileName = "UpdateInfo.json"; + + private readonly IFileSystem fs; + private readonly string updateFilePath; + + public UpdateFileStorage(IFileSystem fs) + { + this.fs = fs ?? throw new ArgumentNullException(nameof(fs)); + + updateFilePath = GetFilePathAndEnsureCreated(); + } + + private string GetFilePathAndEnsureCreated() + { + // Use DoNotVerify in case LocalApplicationData doesn’t exist. + string path = fs.Path.Combine(GetFolderPath(SpecialFolder.LocalApplicationData, SpecialFolderOption.DoNotVerify), "UpdateLib", UpdateFileName); + // Ensure the directory and all its parents exist. + fs.Directory.CreateDirectory(path); + + return path; + } + + public async Task LoadAsync() + { + using (var reader = fs.File.OpenText(updateFilePath)) + { + var contents = await reader.ReadToEndAsync(); + return JsonConvert.DeserializeObject(contents); + } + } + + public async Task SaveAsync(UpdateFile file) + { + using (var stream = fs.File.OpenWrite(updateFilePath)) + using (var writer = new StreamWriter(stream)) + { + // truncate + stream.SetLength(0); + + var contents = JsonConvert.SerializeObject(file); + await writer.WriteAsync(contents); + } + } + } +} diff --git a/UpdateLib/UpdateLib.csproj b/UpdateLib/UpdateLib.csproj index f996516..898ec96 100644 --- a/UpdateLib/UpdateLib.csproj +++ b/UpdateLib/UpdateLib.csproj @@ -6,6 +6,7 @@ + diff --git a/UpdateLib/Updater.cs b/UpdateLib/Updater.cs index a138e5f..df23c7e 100644 --- a/UpdateLib/Updater.cs +++ b/UpdateLib/Updater.cs @@ -1,27 +1,39 @@ -using System.Threading.Tasks; +using System; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; using UpdateLib.Abstractions; using UpdateLib.Abstractions.Storage; using UpdateLib.Core; +using UpdateLib.Core.Storage.Files; namespace UpdateLib { public class Updater : IUpdater { - private readonly ICacheStorage cacheStorage; + private readonly ICacheManager cacheManager; + private HashCacheFile cacheFile; - public Updater(ICacheStorage cacheStorage) + public bool IsInitialized { get; private set; } + + public Updater(ICacheManager cacheManager) { - this.cacheStorage = cacheStorage ?? throw new System.ArgumentNullException(nameof(cacheStorage)); + this.cacheManager = cacheManager ?? throw new ArgumentNullException(nameof(cacheManager)); } - public Task CheckForUpdatesAsync() + public async Task CheckForUpdatesAsync() { - throw new System.NotImplementedException(); + if (!IsInitialized) + await InitializeAsync(); + + IHttpClientFactory factory; } public async Task InitializeAsync() { - var cache = await cacheStorage.LoadAsync(); + cacheFile = await cacheManager.UpdateCacheAsync(); + + IsInitialized = true; } } } From ade0a623e6b14e2a4066a43cf2769eeba6694e91 Mon Sep 17 00:00:00 2001 From: Matthias Beerens <3512339+Matthiee@users.noreply.github.com> Date: Mon, 13 Jan 2020 19:53:09 +0100 Subject: [PATCH 09/14] Add catalog file --- .../Common/IO/IDownloadClientService.cs | 11 ++++ .../Storage/IUpdateCatalogStorage.cs | 11 ++++ .../Core/Common/IO/DownloadClientService.cs | 28 ++++++++++ UpdateLib/Core/Storage/Files/HashCacheFile.cs | 1 + .../Core/Storage/Files/UpdateCatalogFile.cs | 32 +++++++++++ .../Core/Storage/UpdateCatalogStorage.cs | 55 +++++++++++++++++++ UpdateLib/UpdateLib.csproj | 1 + UpdateLib/Updater.cs | 5 +- 8 files changed, 140 insertions(+), 4 deletions(-) create mode 100644 UpdateLib/Abstractions/Common/IO/IDownloadClientService.cs create mode 100644 UpdateLib/Abstractions/Storage/IUpdateCatalogStorage.cs create mode 100644 UpdateLib/Core/Common/IO/DownloadClientService.cs create mode 100644 UpdateLib/Core/Storage/Files/UpdateCatalogFile.cs create mode 100644 UpdateLib/Core/Storage/UpdateCatalogStorage.cs diff --git a/UpdateLib/Abstractions/Common/IO/IDownloadClientService.cs b/UpdateLib/Abstractions/Common/IO/IDownloadClientService.cs new file mode 100644 index 0000000..58804aa --- /dev/null +++ b/UpdateLib/Abstractions/Common/IO/IDownloadClientService.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using UpdateLib.Core.Storage.Files; + +namespace UpdateLib.Abstractions.Common.IO +{ + public interface IDownloadClientService + { + Task GetUpdateFileAsync(); + Task GetUpdateCatalogFileAsync(); + } +} diff --git a/UpdateLib/Abstractions/Storage/IUpdateCatalogStorage.cs b/UpdateLib/Abstractions/Storage/IUpdateCatalogStorage.cs new file mode 100644 index 0000000..157ebc9 --- /dev/null +++ b/UpdateLib/Abstractions/Storage/IUpdateCatalogStorage.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using UpdateLib.Core.Storage.Files; + +namespace UpdateLib.Abstractions.Storage +{ + public interface IUpdateCatalogStorage + { + Task LoadAsync(); + Task SaveAsync(UpdateCatalogFile file); + } +} diff --git a/UpdateLib/Core/Common/IO/DownloadClientService.cs b/UpdateLib/Core/Common/IO/DownloadClientService.cs new file mode 100644 index 0000000..99100d3 --- /dev/null +++ b/UpdateLib/Core/Common/IO/DownloadClientService.cs @@ -0,0 +1,28 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using UpdateLib.Abstractions.Common.IO; +using UpdateLib.Core.Storage.Files; + +namespace UpdateLib.Core.Common.IO +{ + public class DownloadClientService : IDownloadClientService + { + private readonly HttpClient httpClient; + + public DownloadClientService(HttpClient httpClient) + { + this.httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); + } + + public Task GetUpdateCatalogFileAsync() + { + throw new NotImplementedException(); + } + + public Task GetUpdateFileAsync() + { + throw new NotImplementedException(); + } + } +} diff --git a/UpdateLib/Core/Storage/Files/HashCacheFile.cs b/UpdateLib/Core/Storage/Files/HashCacheFile.cs index 7d6bcd4..7ea9792 100644 --- a/UpdateLib/Core/Storage/Files/HashCacheFile.cs +++ b/UpdateLib/Core/Storage/Files/HashCacheFile.cs @@ -5,6 +5,7 @@ namespace UpdateLib.Core.Storage.Files { public class HashCacheFile { + public UpdateVersion Version { get; set; } public List Entries { get; set; } public HashCacheFile() diff --git a/UpdateLib/Core/Storage/Files/UpdateCatalogFile.cs b/UpdateLib/Core/Storage/Files/UpdateCatalogFile.cs new file mode 100644 index 0000000..874a877 --- /dev/null +++ b/UpdateLib/Core/Storage/Files/UpdateCatalogFile.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using UpdateLib.Core.Common; + +namespace UpdateLib.Core.Storage.Files +{ + public class UpdateCatalogFile + { + /// + /// Gets the Catalog + /// + public List Catalog { get; private set; } = new List(); + + /// + /// Download Url's + /// + public List DownloadUrls { get; private set; } = new List(); + + /// + /// Gets the best update for the current version. + /// + /// The currect application version + /// + public UpdateInfo GetLatestUpdateForVersion(UpdateVersion currentVersion) + { + if (currentVersion == null) throw new ArgumentNullException(nameof(currentVersion)); + + return Catalog.OrderBy(c => c).Where(c => currentVersion < c.Version && ((c.IsPatch && c.BasedOnVersion == currentVersion) || !c.IsPatch)).FirstOrDefault(); + } + } +} diff --git a/UpdateLib/Core/Storage/UpdateCatalogStorage.cs b/UpdateLib/Core/Storage/UpdateCatalogStorage.cs new file mode 100644 index 0000000..1928bf8 --- /dev/null +++ b/UpdateLib/Core/Storage/UpdateCatalogStorage.cs @@ -0,0 +1,55 @@ +using Newtonsoft.Json; +using System.IO; +using System.IO.Abstractions; +using System.Threading.Tasks; +using UpdateLib.Abstractions.Storage; +using UpdateLib.Core.Storage.Files; +using static System.Environment; + +namespace UpdateLib.Core.Storage +{ + public class UpdateCatalogStorage : IUpdateCatalogStorage + { + private readonly IFileSystem fs; + private readonly string catalogFilePath; + + public UpdateCatalogStorage(IFileSystem fs) + { + this.fs = fs ?? throw new System.ArgumentNullException(nameof(fs)); + + catalogFilePath = GetFilePathAndEnsureCreated(); + } + + private string GetFilePathAndEnsureCreated() + { + // Use DoNotVerify in case LocalApplicationData doesn’t exist. + string path = fs.Path.Combine(GetFolderPath(SpecialFolder.LocalApplicationData, SpecialFolderOption.DoNotVerify), "UpdateLib", "Cache", "UpdateCatalogus.json"); + // Ensure the directory and all its parents exist. + fs.Directory.CreateDirectory(path); + + return path; + } + + public async Task LoadAsync() + { + using (var reader = fs.File.OpenText(catalogFilePath)) + { + var contents = await reader.ReadToEndAsync(); + return JsonConvert.DeserializeObject(contents); + } + } + + public async Task SaveAsync(UpdateCatalogFile file) + { + using (var stream = fs.File.OpenWrite(catalogFilePath)) + using (var writer = new StreamWriter(stream)) + { + // truncate + stream.SetLength(0); + + var contents = JsonConvert.SerializeObject(file); + await writer.WriteAsync(contents); + } + } + } +} diff --git a/UpdateLib/UpdateLib.csproj b/UpdateLib/UpdateLib.csproj index 898ec96..fe85853 100644 --- a/UpdateLib/UpdateLib.csproj +++ b/UpdateLib/UpdateLib.csproj @@ -6,6 +6,7 @@ + diff --git a/UpdateLib/Updater.cs b/UpdateLib/Updater.cs index df23c7e..fd8b9bf 100644 --- a/UpdateLib/Updater.cs +++ b/UpdateLib/Updater.cs @@ -1,9 +1,6 @@ using System; -using System.Net; -using System.Net.Http; using System.Threading.Tasks; using UpdateLib.Abstractions; -using UpdateLib.Abstractions.Storage; using UpdateLib.Core; using UpdateLib.Core.Storage.Files; @@ -26,7 +23,7 @@ public async Task CheckForUpdatesAsync() if (!IsInitialized) await InitializeAsync(); - IHttpClientFactory factory; + return null; } public async Task InitializeAsync() From aa99250a8f3a778666ec515f1f471d9a25231100 Mon Sep 17 00:00:00 2001 From: Matthias Beerens <3512339+Matthiee@users.noreply.github.com> Date: Mon, 13 Jan 2020 20:25:51 +0100 Subject: [PATCH 10/14] Added UpdateCatalogManager code --- .../Abstractions/IUpdateCatalogManager.cs | 10 ++++ .../Abstractions/Storage/ICacheStorage.cs | 1 + .../Storage/IUpdateCatalogStorage.cs | 5 +- UpdateLib/Core/CacheManager.cs | 15 ++++-- UpdateLib/Core/Storage/CacheStorage.cs | 4 ++ .../Core/Storage/UpdateCatalogStorage.cs | 5 ++ UpdateLib/Core/UpdateCatalogManager.cs | 51 +++++++++++++++++++ 7 files changed, 86 insertions(+), 5 deletions(-) create mode 100644 UpdateLib/Abstractions/IUpdateCatalogManager.cs create mode 100644 UpdateLib/Core/UpdateCatalogManager.cs diff --git a/UpdateLib/Abstractions/IUpdateCatalogManager.cs b/UpdateLib/Abstractions/IUpdateCatalogManager.cs new file mode 100644 index 0000000..88f627e --- /dev/null +++ b/UpdateLib/Abstractions/IUpdateCatalogManager.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using UpdateLib.Core.Storage.Files; + +namespace UpdateLib.Abstractions +{ + public interface IUpdateCatalogManager + { + Task GetUpdateCatalogFileAsync(); + } +} diff --git a/UpdateLib/Abstractions/Storage/ICacheStorage.cs b/UpdateLib/Abstractions/Storage/ICacheStorage.cs index f3f956f..e8d257c 100644 --- a/UpdateLib/Abstractions/Storage/ICacheStorage.cs +++ b/UpdateLib/Abstractions/Storage/ICacheStorage.cs @@ -6,6 +6,7 @@ namespace UpdateLib.Abstractions.Storage { public interface ICacheStorage { + bool CacheExists { get; } Task SaveAsync(HashCacheFile file); Task LoadAsync(); } diff --git a/UpdateLib/Abstractions/Storage/IUpdateCatalogStorage.cs b/UpdateLib/Abstractions/Storage/IUpdateCatalogStorage.cs index 157ebc9..e71b25b 100644 --- a/UpdateLib/Abstractions/Storage/IUpdateCatalogStorage.cs +++ b/UpdateLib/Abstractions/Storage/IUpdateCatalogStorage.cs @@ -1,10 +1,13 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; using UpdateLib.Core.Storage.Files; namespace UpdateLib.Abstractions.Storage { public interface IUpdateCatalogStorage { + bool Exists { get; } + DateTime LastWriteTime { get; } Task LoadAsync(); Task SaveAsync(UpdateCatalogFile file); } diff --git a/UpdateLib/Core/CacheManager.cs b/UpdateLib/Core/CacheManager.cs index d8b1399..7ed752a 100644 --- a/UpdateLib/Core/CacheManager.cs +++ b/UpdateLib/Core/CacheManager.cs @@ -30,13 +30,20 @@ public async Task UpdateCacheAsync() { HashCacheFile file = null; - try + if (cacheStorage.CacheExists) { - file = await cacheStorage.LoadAsync(); + try + { + file = await cacheStorage.LoadAsync(); + } + catch (Exception e) + { + logger.LogError(e, "Unable to load cache from storage"); + } } - catch (Exception e) + else { - logger.LogError(e, "Unable to load cache from storage"); + logger.LogDebug($"Cache file doesn't exist"); } files = fs.DirectoryInfo.FromDirectoryName(".").GetFiles("*", SearchOption.AllDirectories).Where(f => !f.FullName.Contains(".old.tmp")); diff --git a/UpdateLib/Core/Storage/CacheStorage.cs b/UpdateLib/Core/Storage/CacheStorage.cs index 5cf306c..c9a98a7 100644 --- a/UpdateLib/Core/Storage/CacheStorage.cs +++ b/UpdateLib/Core/Storage/CacheStorage.cs @@ -17,6 +17,8 @@ public class CacheStorage : ICacheStorage private readonly IFileSystem fs; private readonly string cachePath; + public bool CacheExists => fs.File.Exists(cachePath); + public CacheStorage(IFileSystem storage) { this.fs = storage ?? throw new ArgumentNullException(nameof(storage)); @@ -36,6 +38,8 @@ private string GetFilePathAndEnsureCreated() public async Task LoadAsync() { + if (!CacheExists) throw new FileNotFoundException("File doesn't exist", cachePath); + using (var reader = fs.File.OpenText(cachePath)) { var contents = await reader.ReadToEndAsync(); diff --git a/UpdateLib/Core/Storage/UpdateCatalogStorage.cs b/UpdateLib/Core/Storage/UpdateCatalogStorage.cs index 1928bf8..996f957 100644 --- a/UpdateLib/Core/Storage/UpdateCatalogStorage.cs +++ b/UpdateLib/Core/Storage/UpdateCatalogStorage.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using System; using System.IO; using System.IO.Abstractions; using System.Threading.Tasks; @@ -13,6 +14,10 @@ public class UpdateCatalogStorage : IUpdateCatalogStorage private readonly IFileSystem fs; private readonly string catalogFilePath; + public bool Exists => fs.File.Exists(catalogFilePath); + + public DateTime LastWriteTime => fs.File.GetLastWriteTimeUtc(catalogFilePath); + public UpdateCatalogStorage(IFileSystem fs) { this.fs = fs ?? throw new System.ArgumentNullException(nameof(fs)); diff --git a/UpdateLib/Core/UpdateCatalogManager.cs b/UpdateLib/Core/UpdateCatalogManager.cs new file mode 100644 index 0000000..d32ab0a --- /dev/null +++ b/UpdateLib/Core/UpdateCatalogManager.cs @@ -0,0 +1,51 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Threading.Tasks; +using UpdateLib.Abstractions; +using UpdateLib.Abstractions.Common.IO; +using UpdateLib.Abstractions.Storage; +using UpdateLib.Core.Storage.Files; + +namespace UpdateLib.Core +{ + public class UpdateCatalogManager : IUpdateCatalogManager + { + private readonly ILogger logger; + private readonly IDownloadClientService downloadClient; + private readonly IUpdateCatalogStorage storage; + + public UpdateCatalogManager(ILogger logger, IDownloadClientService downloadClient, IUpdateCatalogStorage storage) + { + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + this.downloadClient = downloadClient ?? throw new ArgumentNullException(nameof(downloadClient)); + this.storage = storage ?? throw new ArgumentNullException(nameof(storage)); + } + + public async Task GetUpdateCatalogFileAsync() + { + logger.LogInformation("Getting update catalog file"); + + if (!storage.Exists || storage.LastWriteTime.AddMinutes(5) < DateTime.UtcNow) + { + logger.LogDebug("Downloading remote update catalog file"); + + return await DownloadRemoteCatalogFileAsync(); + } + else + { + logger.LogDebug("Loading local update catalog file"); + + return await storage.LoadAsync(); + } + } + + private async Task DownloadRemoteCatalogFileAsync() + { + var file = await downloadClient.GetUpdateCatalogFileAsync(); + + await storage.SaveAsync(file); + + return file; + } + } +} From 9e921ad1691d6b180c2a21ef2a00aaa0484792bd Mon Sep 17 00:00:00 2001 From: Matthias Beerens <3512339+Matthiee@users.noreply.github.com> Date: Sat, 18 Jan 2020 22:08:36 +0100 Subject: [PATCH 11/14] Add Test for updatecatalogmanager --- UpdateLib.Tests/Core/CacheManagerTests.cs | 2 +- .../Core/UpdataCatalogManagerTests.cs | 99 +++++++++++++++++++ UpdateLib.Tests/UpdateLib.Tests.csproj | 1 + UpdateLib/Core/Storage/CacheStorage.cs | 2 +- .../Core/Storage/UpdateCatalogStorage.cs | 2 +- UpdateLib/Core/Storage/UpdateFileStorage.cs | 2 +- UpdateLib/Core/UpdateCatalogManager.cs | 13 ++- 7 files changed, 114 insertions(+), 7 deletions(-) create mode 100644 UpdateLib.Tests/Core/UpdataCatalogManagerTests.cs diff --git a/UpdateLib.Tests/Core/CacheManagerTests.cs b/UpdateLib.Tests/Core/CacheManagerTests.cs index aad62c5..9072a43 100644 --- a/UpdateLib.Tests/Core/CacheManagerTests.cs +++ b/UpdateLib.Tests/Core/CacheManagerTests.cs @@ -34,7 +34,7 @@ public async Task NonExistingCacheCreatesANewOne() [Fact] public async Task OldCacheEntriesAreDeleted() { - var fs = new MockFileSystem(); + var fs = new MockFileSystem(new Dictionary(), "C:\\app"); var cache = new CacheStorage(fs); var file = new HashCacheFile(); diff --git a/UpdateLib.Tests/Core/UpdataCatalogManagerTests.cs b/UpdateLib.Tests/Core/UpdataCatalogManagerTests.cs new file mode 100644 index 0000000..652f974 --- /dev/null +++ b/UpdateLib.Tests/Core/UpdataCatalogManagerTests.cs @@ -0,0 +1,99 @@ +using Moq; +using System; +using System.Collections.Generic; +using System.IO.Abstractions; +using System.Linq; +using System.Threading.Tasks; +using UpdateLib.Abstractions.Common.IO; +using UpdateLib.Abstractions.Storage; +using UpdateLib.Core; +using UpdateLib.Core.Storage.Files; +using Xunit; +using static UpdateLib.Tests.Helpers; + +namespace UpdateLib.Tests.Core +{ + public class UpdataCatalogManagerTests + { + [Fact] + public async Task DownloadsRemoteFileWhenLocalFileDoesNotExist() + { + var downloadClient = new Mock(); + var storage = new Mock(); + var catalogus = new UpdateCatalogFile(); + + storage.SetupGet(_ => _.Exists).Returns(true); + downloadClient.Setup(_ => _.GetUpdateCatalogFileAsync()).ReturnsAsync(catalogus).Verifiable(); + + var mgr = new UpdateCatalogManager(CreateLogger(), downloadClient.Object, storage.Object); + + var result = await mgr.GetUpdateCatalogFileAsync(); + + Assert.Equal(catalogus, result); + + downloadClient.Verify(); + } + + [Fact] + public async Task DownloadsRemoteFileWhenLocalFileDoesExistButIsTooOld() + { + var downloadClient = new Mock(); + var storage = new Mock(); + var catalogus = new UpdateCatalogFile(); + + storage.SetupGet(_ => _.Exists).Returns(true); + storage.SetupGet(_ => _.LastWriteTime).Returns(DateTime.UtcNow.Subtract(TimeSpan.FromMinutes(10))); + downloadClient.Setup(_ => _.GetUpdateCatalogFileAsync()).ReturnsAsync(catalogus).Verifiable(); + + var mgr = new UpdateCatalogManager(CreateLogger(), downloadClient.Object, storage.Object); + + var result = await mgr.GetUpdateCatalogFileAsync(); + + Assert.Equal(catalogus, result); + + downloadClient.Verify(); + } + + [Fact] + public async Task LoadsLocalFileWhenItExists() + { + var downloadClient = new Mock(); + var storage = new Mock(); + var catalogus = new UpdateCatalogFile(); + + storage.SetupGet(_ => _.Exists).Returns(true); + storage.SetupGet(_ => _.LastWriteTime).Returns(DateTime.UtcNow); + storage.Setup(_ => _.LoadAsync()).ReturnsAsync(catalogus).Verifiable(); + + var mgr = new UpdateCatalogManager(CreateLogger(), downloadClient.Object, storage.Object); + + var result = await mgr.GetUpdateCatalogFileAsync(); + + Assert.Equal(catalogus, result); + + storage.Verify(); + } + + [Fact] + public async Task DownloadsRemoteFileWhenLocalFileFailed() + { + var downloadClient = new Mock(); + var storage = new Mock(); + var catalogus = new UpdateCatalogFile(); + + storage.SetupGet(_ => _.Exists).Returns(true); + storage.SetupGet(_ => _.LastWriteTime).Returns(DateTime.UtcNow); + storage.Setup(_ => _.LoadAsync()).Throws(new Exception()).Verifiable(); + downloadClient.Setup(_ => _.GetUpdateCatalogFileAsync()).ReturnsAsync(catalogus).Verifiable(); + + var mgr = new UpdateCatalogManager(CreateLogger(), downloadClient.Object, storage.Object); + + var result = await mgr.GetUpdateCatalogFileAsync(); + + Assert.Equal(catalogus, result); + + storage.Verify(); + downloadClient.Verify(); + } + } +} diff --git a/UpdateLib.Tests/UpdateLib.Tests.csproj b/UpdateLib.Tests/UpdateLib.Tests.csproj index 6db7da9..229019c 100644 --- a/UpdateLib.Tests/UpdateLib.Tests.csproj +++ b/UpdateLib.Tests/UpdateLib.Tests.csproj @@ -9,6 +9,7 @@ + diff --git a/UpdateLib/Core/Storage/CacheStorage.cs b/UpdateLib/Core/Storage/CacheStorage.cs index c9a98a7..57de19a 100644 --- a/UpdateLib/Core/Storage/CacheStorage.cs +++ b/UpdateLib/Core/Storage/CacheStorage.cs @@ -31,7 +31,7 @@ private string GetFilePathAndEnsureCreated() // Use DoNotVerify in case LocalApplicationData doesn’t exist. string path = fs.Path.Combine(GetFolderPath(SpecialFolder.LocalApplicationData, SpecialFolderOption.DoNotVerify), "UpdateLib", CachePathName, CacheFileName); // Ensure the directory and all its parents exist. - fs.Directory.CreateDirectory(path); + fs.Directory.CreateDirectory(fs.Path.GetDirectoryName(path)); return path; } diff --git a/UpdateLib/Core/Storage/UpdateCatalogStorage.cs b/UpdateLib/Core/Storage/UpdateCatalogStorage.cs index 996f957..f28bed1 100644 --- a/UpdateLib/Core/Storage/UpdateCatalogStorage.cs +++ b/UpdateLib/Core/Storage/UpdateCatalogStorage.cs @@ -30,7 +30,7 @@ private string GetFilePathAndEnsureCreated() // Use DoNotVerify in case LocalApplicationData doesn’t exist. string path = fs.Path.Combine(GetFolderPath(SpecialFolder.LocalApplicationData, SpecialFolderOption.DoNotVerify), "UpdateLib", "Cache", "UpdateCatalogus.json"); // Ensure the directory and all its parents exist. - fs.Directory.CreateDirectory(path); + fs.Directory.CreateDirectory(fs.Path.GetDirectoryName(path)); return path; } diff --git a/UpdateLib/Core/Storage/UpdateFileStorage.cs b/UpdateLib/Core/Storage/UpdateFileStorage.cs index 8117ea8..482806d 100644 --- a/UpdateLib/Core/Storage/UpdateFileStorage.cs +++ b/UpdateLib/Core/Storage/UpdateFileStorage.cs @@ -28,7 +28,7 @@ private string GetFilePathAndEnsureCreated() // Use DoNotVerify in case LocalApplicationData doesn’t exist. string path = fs.Path.Combine(GetFolderPath(SpecialFolder.LocalApplicationData, SpecialFolderOption.DoNotVerify), "UpdateLib", UpdateFileName); // Ensure the directory and all its parents exist. - fs.Directory.CreateDirectory(path); + fs.Directory.CreateDirectory(fs.Path.GetDirectoryName(path)); return path; } diff --git a/UpdateLib/Core/UpdateCatalogManager.cs b/UpdateLib/Core/UpdateCatalogManager.cs index d32ab0a..2c22e5e 100644 --- a/UpdateLib/Core/UpdateCatalogManager.cs +++ b/UpdateLib/Core/UpdateCatalogManager.cs @@ -27,20 +27,27 @@ public async Task GetUpdateCatalogFileAsync() if (!storage.Exists || storage.LastWriteTime.AddMinutes(5) < DateTime.UtcNow) { - logger.LogDebug("Downloading remote update catalog file"); - return await DownloadRemoteCatalogFileAsync(); } - else + + try { logger.LogDebug("Loading local update catalog file"); return await storage.LoadAsync(); } + catch (Exception e) + { + logger.LogError(e, "Unable to load local update catalog file"); + + return await DownloadRemoteCatalogFileAsync(); + } } private async Task DownloadRemoteCatalogFileAsync() { + logger.LogDebug("Downloading remote update catalog file"); + var file = await downloadClient.GetUpdateCatalogFileAsync(); await storage.SaveAsync(file); From cd29056f872e6ac5aa3cde68601e94ddab60900a Mon Sep 17 00:00:00 2001 From: Matthias Beerens <3512339+Matthiee@users.noreply.github.com> Date: Sun, 19 Jan 2020 14:20:59 +0100 Subject: [PATCH 12/14] Generalize storage --- UpdateLib.Tests/Core/CacheManagerTests.cs | 5 +- UpdateLib/Abstractions/Storage/BaseStorage.cs | 64 +++++++++++++++++++ .../Abstractions/Storage/ICacheStorage.cs | 8 +-- UpdateLib/Abstractions/Storage/IStorage.cs | 11 ++++ .../Storage/IUpdateCatalogStorage.cs | 5 +- .../Storage/IUpdateFileStorage.cs | 7 +- UpdateLib/Core/Storage/CacheStorage.cs | 52 ++------------- .../Core/Storage/UpdateCatalogStorage.cs | 51 ++------------- UpdateLib/Core/Storage/UpdateFileStorage.cs | 48 +------------- 9 files changed, 95 insertions(+), 156 deletions(-) create mode 100644 UpdateLib/Abstractions/Storage/BaseStorage.cs create mode 100644 UpdateLib/Abstractions/Storage/IStorage.cs diff --git a/UpdateLib.Tests/Core/CacheManagerTests.cs b/UpdateLib.Tests/Core/CacheManagerTests.cs index 9072a43..dc5bb35 100644 --- a/UpdateLib.Tests/Core/CacheManagerTests.cs +++ b/UpdateLib.Tests/Core/CacheManagerTests.cs @@ -87,11 +87,10 @@ public async Task UpdateCacheAddsNewEntries_TempFilesAreIgnored() public async Task CorruptCacheFileGetsRestored() { var fs = new MockFileSystem(); - string path = fs.Path.Combine(GetFolderPath(SpecialFolder.LocalApplicationData, SpecialFolderOption.DoNotVerify), "UpdateLib", "Cache", "FileCache.json"); + var cache = new CacheStorage(fs); - fs.AddFile(path, new MockFileData("blabla")); // not valid json + fs.AddFile(cache.FileInfo.FullName, new MockFileData("blabla")); // not valid json - var cache = new CacheStorage(fs); var manager = CreateCacheManager(fs, cache); await manager.UpdateCacheAsync(); diff --git a/UpdateLib/Abstractions/Storage/BaseStorage.cs b/UpdateLib/Abstractions/Storage/BaseStorage.cs new file mode 100644 index 0000000..37b4d8f --- /dev/null +++ b/UpdateLib/Abstractions/Storage/BaseStorage.cs @@ -0,0 +1,64 @@ +using Newtonsoft.Json; +using System; +using System.IO; +using System.IO.Abstractions; +using System.Threading.Tasks; +using static System.Environment; + +namespace UpdateLib.Abstractions.Storage +{ + public abstract class BaseStorage : IStorage + { + private readonly string localPath; + private readonly IFileSystem fs; + + public BaseStorage(IFileSystem fs, string appName, string dirName, string fileName) + { + this.fs = fs ?? throw new System.ArgumentNullException(nameof(fs)); + + localPath = GetFilePathAndEnsureCreated(appName, dirName, fileName); + + FileInfo = fs.FileInfo.FromFileName(localPath); + } + + protected string GetFilePathAndEnsureCreated(string appName, string dirName, string fileName) + { + // Use DoNotVerify in case LocalApplicationData doesn’t exist. + string path = fs.Path.Combine(GetFolderPath(SpecialFolder.LocalApplicationData, SpecialFolderOption.DoNotVerify), appName, dirName, fileName); + // Ensure the directory and all its parents exist. + fs.Directory.CreateDirectory(fs.Path.GetDirectoryName(path)); + + return path; + } + + public IFileInfo FileInfo { get; protected set; } + + public virtual async Task LoadAsync() + { + if (!FileInfo.Exists) + throw new FileNotFoundException("File not found", FileInfo.Name); + + using (var reader = fs.File.OpenText(localPath)) + { + var contents = await reader.ReadToEndAsync(); + return JsonConvert.DeserializeObject(contents); + } + } + + public virtual async Task SaveAsync(TFile file) + { + if (FileInfo.Exists && FileInfo.IsReadOnly) + throw new InvalidOperationException($"Writing to read-only file is not allowed '{FileInfo.FullName}'"); + + using (var stream = fs.File.OpenWrite(localPath)) + using (var writer = new StreamWriter(stream)) + { + // truncate + stream.SetLength(0); + + var contents = JsonConvert.SerializeObject(file); + await writer.WriteAsync(contents); + } + } + } +} diff --git a/UpdateLib/Abstractions/Storage/ICacheStorage.cs b/UpdateLib/Abstractions/Storage/ICacheStorage.cs index e8d257c..55d350c 100644 --- a/UpdateLib/Abstractions/Storage/ICacheStorage.cs +++ b/UpdateLib/Abstractions/Storage/ICacheStorage.cs @@ -1,13 +1,9 @@ -using System; -using System.Threading.Tasks; -using UpdateLib.Core.Storage.Files; +using UpdateLib.Core.Storage.Files; namespace UpdateLib.Abstractions.Storage { - public interface ICacheStorage + public interface ICacheStorage : IStorage { bool CacheExists { get; } - Task SaveAsync(HashCacheFile file); - Task LoadAsync(); } } diff --git a/UpdateLib/Abstractions/Storage/IStorage.cs b/UpdateLib/Abstractions/Storage/IStorage.cs new file mode 100644 index 0000000..89dc6c4 --- /dev/null +++ b/UpdateLib/Abstractions/Storage/IStorage.cs @@ -0,0 +1,11 @@ +using System.IO.Abstractions; +using System.Threading.Tasks; + +namespace UpdateLib.Abstractions.Storage +{ + public interface IStorage + { + Task LoadAsync(); + Task SaveAsync(TFile file); + } +} diff --git a/UpdateLib/Abstractions/Storage/IUpdateCatalogStorage.cs b/UpdateLib/Abstractions/Storage/IUpdateCatalogStorage.cs index e71b25b..cfeffab 100644 --- a/UpdateLib/Abstractions/Storage/IUpdateCatalogStorage.cs +++ b/UpdateLib/Abstractions/Storage/IUpdateCatalogStorage.cs @@ -1,14 +1,11 @@ using System; -using System.Threading.Tasks; using UpdateLib.Core.Storage.Files; namespace UpdateLib.Abstractions.Storage { - public interface IUpdateCatalogStorage + public interface IUpdateCatalogStorage : IStorage { bool Exists { get; } DateTime LastWriteTime { get; } - Task LoadAsync(); - Task SaveAsync(UpdateCatalogFile file); } } diff --git a/UpdateLib/Abstractions/Storage/IUpdateFileStorage.cs b/UpdateLib/Abstractions/Storage/IUpdateFileStorage.cs index 3c5f470..3863459 100644 --- a/UpdateLib/Abstractions/Storage/IUpdateFileStorage.cs +++ b/UpdateLib/Abstractions/Storage/IUpdateFileStorage.cs @@ -1,11 +1,8 @@ -using System.Threading.Tasks; -using UpdateLib.Core.Storage.Files; +using UpdateLib.Core.Storage.Files; namespace UpdateLib.Abstractions.Storage { - interface IUpdateFileStorage + interface IUpdateFileStorage : IStorage { - Task SaveAsync(UpdateFile file); - Task LoadAsync(); } } diff --git a/UpdateLib/Core/Storage/CacheStorage.cs b/UpdateLib/Core/Storage/CacheStorage.cs index 57de19a..e71811e 100644 --- a/UpdateLib/Core/Storage/CacheStorage.cs +++ b/UpdateLib/Core/Storage/CacheStorage.cs @@ -1,63 +1,19 @@ -using Newtonsoft.Json; -using System; -using System.IO; -using System.IO.Abstractions; -using System.Threading.Tasks; +using System.IO.Abstractions; using UpdateLib.Abstractions.Storage; using UpdateLib.Core.Storage.Files; -using static System.Environment; namespace UpdateLib.Core.Storage { - public class CacheStorage : ICacheStorage + public class CacheStorage : BaseStorage, ICacheStorage { private const string CachePathName = "Cache"; private const string CacheFileName = "FileCache.json"; - private readonly IFileSystem fs; - private readonly string cachePath; - - public bool CacheExists => fs.File.Exists(cachePath); + public bool CacheExists => FileInfo.Exists; public CacheStorage(IFileSystem storage) + : base(storage, "UpdateLib", CachePathName, CacheFileName) { - this.fs = storage ?? throw new ArgumentNullException(nameof(storage)); - - cachePath = GetFilePathAndEnsureCreated(); - } - - private string GetFilePathAndEnsureCreated() - { - // Use DoNotVerify in case LocalApplicationData doesn’t exist. - string path = fs.Path.Combine(GetFolderPath(SpecialFolder.LocalApplicationData, SpecialFolderOption.DoNotVerify), "UpdateLib", CachePathName, CacheFileName); - // Ensure the directory and all its parents exist. - fs.Directory.CreateDirectory(fs.Path.GetDirectoryName(path)); - - return path; - } - - public async Task LoadAsync() - { - if (!CacheExists) throw new FileNotFoundException("File doesn't exist", cachePath); - - using (var reader = fs.File.OpenText(cachePath)) - { - var contents = await reader.ReadToEndAsync(); - return JsonConvert.DeserializeObject(contents); - } - } - - public async Task SaveAsync(HashCacheFile file) - { - using (var stream = fs.File.OpenWrite(cachePath)) - using (var writer = new StreamWriter(stream)) - { - // truncate - stream.SetLength(0); - - var contents = JsonConvert.SerializeObject(file); - await writer.WriteAsync(contents); - } } } } diff --git a/UpdateLib/Core/Storage/UpdateCatalogStorage.cs b/UpdateLib/Core/Storage/UpdateCatalogStorage.cs index f28bed1..179be87 100644 --- a/UpdateLib/Core/Storage/UpdateCatalogStorage.cs +++ b/UpdateLib/Core/Storage/UpdateCatalogStorage.cs @@ -1,60 +1,21 @@ -using Newtonsoft.Json; -using System; -using System.IO; +using System; using System.IO.Abstractions; -using System.Threading.Tasks; using UpdateLib.Abstractions.Storage; using UpdateLib.Core.Storage.Files; -using static System.Environment; namespace UpdateLib.Core.Storage { - public class UpdateCatalogStorage : IUpdateCatalogStorage + public class UpdateCatalogStorage : BaseStorage, IUpdateCatalogStorage { - private readonly IFileSystem fs; - private readonly string catalogFilePath; + private const string FileName = "UpdateCatalogus.json"; - public bool Exists => fs.File.Exists(catalogFilePath); + public bool Exists => FileInfo.Exists; - public DateTime LastWriteTime => fs.File.GetLastWriteTimeUtc(catalogFilePath); + public DateTime LastWriteTime => FileInfo.LastWriteTimeUtc; public UpdateCatalogStorage(IFileSystem fs) + : base(fs, "UpdateLib", "Cache", FileName) { - this.fs = fs ?? throw new System.ArgumentNullException(nameof(fs)); - - catalogFilePath = GetFilePathAndEnsureCreated(); - } - - private string GetFilePathAndEnsureCreated() - { - // Use DoNotVerify in case LocalApplicationData doesn’t exist. - string path = fs.Path.Combine(GetFolderPath(SpecialFolder.LocalApplicationData, SpecialFolderOption.DoNotVerify), "UpdateLib", "Cache", "UpdateCatalogus.json"); - // Ensure the directory and all its parents exist. - fs.Directory.CreateDirectory(fs.Path.GetDirectoryName(path)); - - return path; - } - - public async Task LoadAsync() - { - using (var reader = fs.File.OpenText(catalogFilePath)) - { - var contents = await reader.ReadToEndAsync(); - return JsonConvert.DeserializeObject(contents); - } - } - - public async Task SaveAsync(UpdateCatalogFile file) - { - using (var stream = fs.File.OpenWrite(catalogFilePath)) - using (var writer = new StreamWriter(stream)) - { - // truncate - stream.SetLength(0); - - var contents = JsonConvert.SerializeObject(file); - await writer.WriteAsync(contents); - } } } } diff --git a/UpdateLib/Core/Storage/UpdateFileStorage.cs b/UpdateLib/Core/Storage/UpdateFileStorage.cs index 482806d..73bd33b 100644 --- a/UpdateLib/Core/Storage/UpdateFileStorage.cs +++ b/UpdateLib/Core/Storage/UpdateFileStorage.cs @@ -1,58 +1,16 @@ -using Newtonsoft.Json; -using System; -using System.IO; -using System.IO.Abstractions; -using System.Threading.Tasks; +using System.IO.Abstractions; using UpdateLib.Abstractions.Storage; using UpdateLib.Core.Storage.Files; -using static System.Environment; namespace UpdateLib.Core.Storage { - public class UpdateFileStorage : IUpdateFileStorage + public class UpdateFileStorage : BaseStorage, IUpdateFileStorage { private const string UpdateFileName = "UpdateInfo.json"; - private readonly IFileSystem fs; - private readonly string updateFilePath; - public UpdateFileStorage(IFileSystem fs) + : base(fs, "UpdateLib", string.Empty, UpdateFileName) { - this.fs = fs ?? throw new ArgumentNullException(nameof(fs)); - - updateFilePath = GetFilePathAndEnsureCreated(); - } - - private string GetFilePathAndEnsureCreated() - { - // Use DoNotVerify in case LocalApplicationData doesn’t exist. - string path = fs.Path.Combine(GetFolderPath(SpecialFolder.LocalApplicationData, SpecialFolderOption.DoNotVerify), "UpdateLib", UpdateFileName); - // Ensure the directory and all its parents exist. - fs.Directory.CreateDirectory(fs.Path.GetDirectoryName(path)); - - return path; - } - - public async Task LoadAsync() - { - using (var reader = fs.File.OpenText(updateFilePath)) - { - var contents = await reader.ReadToEndAsync(); - return JsonConvert.DeserializeObject(contents); - } - } - - public async Task SaveAsync(UpdateFile file) - { - using (var stream = fs.File.OpenWrite(updateFilePath)) - using (var writer = new StreamWriter(stream)) - { - // truncate - stream.SetLength(0); - - var contents = JsonConvert.SerializeObject(file); - await writer.WriteAsync(contents); - } } } } From 3f991b3d75ca487dd7ad28da116a9bc2b7e5ace3 Mon Sep 17 00:00:00 2001 From: Matthias Beerens <3512339+Matthiee@users.noreply.github.com> Date: Sun, 2 Feb 2020 17:31:52 +0100 Subject: [PATCH 13/14] Add UpdateFileStorage tests --- .../Core/Common/IO/DirectoryEntryTests.cs | 49 ------------ .../Core/Common/IO/FileEntryTests.cs | 4 +- .../Core/Storage/CacheStorageTests.cs | 3 - .../Core/Storage/UpdateCatalogStorageTests.cs | 37 +++++++++ .../Core/Storage/UpdateFileStorageTests.cs | 39 +++++++++ UpdateLib/Core/Common/IO/DirectoryEntry.cs | 80 +++++-------------- UpdateLib/Core/Common/IO/FileEntry.cs | 39 +-------- 7 files changed, 98 insertions(+), 153 deletions(-) create mode 100644 UpdateLib.Tests/Core/Storage/UpdateCatalogStorageTests.cs create mode 100644 UpdateLib.Tests/Core/Storage/UpdateFileStorageTests.cs diff --git a/UpdateLib.Tests/Core/Common/IO/DirectoryEntryTests.cs b/UpdateLib.Tests/Core/Common/IO/DirectoryEntryTests.cs index f5e9f34..d1a2100 100644 --- a/UpdateLib.Tests/Core/Common/IO/DirectoryEntryTests.cs +++ b/UpdateLib.Tests/Core/Common/IO/DirectoryEntryTests.cs @@ -27,55 +27,6 @@ public void CountReturnsCorrectAmountOfFiles() Assert.Equal(9, root.GetItems().Count()); } - [Fact] - public void CountReturnsCorrectAmountOfFilesAfterDelete() - { - var root = CreateDirectoryWithFiles("1", 2); - var dir2 = CreateDirectoryWithFiles("2", 0); - var dir3 = CreateDirectoryWithFiles("3", 1); - var dir4 = CreateDirectoryWithFiles("4", 4); - var dir5 = CreateDirectoryWithFiles("5", 2); - - root.Add(dir2); - root.Add(dir3); - dir2.Add(dir4); - dir3.Add(dir5); - - root.Remove(dir2); - root.Remove(dir3); - - Assert.Equal(2, root.Count); - Assert.Equal(2, root.GetItems().Count()); - } - - [Fact] - public void RecursiveParentShouldEndUpWithRoot() - { - var root = CreateDirectoryWithFiles("1", 2); - var dir2 = CreateDirectoryWithFiles("2", 0); - var dir3 = CreateDirectoryWithFiles("3", 1); - var dir4 = CreateDirectoryWithFiles("4", 4); - var dir5 = CreateDirectoryWithFiles("5", 2); - - root.Add(dir2); - root.Add(dir3); - dir2.Add(dir4); - dir3.Add(dir5); - - var file = new FileEntry("custom"); - - dir5.Add(file); - - DirectoryEntry parent = file.Parent; - - while (parent.Parent != null) - { - parent = parent.Parent; - } - - Assert.Equal(root, parent); - } - private DirectoryEntry CreateDirectoryWithFiles(string name, int filesToCreate) { var dir = new DirectoryEntry(name); diff --git a/UpdateLib.Tests/Core/Common/IO/FileEntryTests.cs b/UpdateLib.Tests/Core/Common/IO/FileEntryTests.cs index 34255b3..8036c88 100644 --- a/UpdateLib.Tests/Core/Common/IO/FileEntryTests.cs +++ b/UpdateLib.Tests/Core/Common/IO/FileEntryTests.cs @@ -16,11 +16,9 @@ public void ShouldGiveCorrectSourceAndDestination() subFolder.Add(file); - string outputSource = "sub/myfile.txt"; string outputDest = "%root%\\sub\\myfile.txt"; - Assert.Equal(outputSource, file.SourceLocation); - Assert.Equal(outputDest, file.DestinationLocation); + Assert.Equal(outputDest, file.Path); } } } diff --git a/UpdateLib.Tests/Core/Storage/CacheStorageTests.cs b/UpdateLib.Tests/Core/Storage/CacheStorageTests.cs index c7e4f70..83b40b7 100644 --- a/UpdateLib.Tests/Core/Storage/CacheStorageTests.cs +++ b/UpdateLib.Tests/Core/Storage/CacheStorageTests.cs @@ -1,13 +1,10 @@ using System; -using System.Collections.Generic; using System.IO.Abstractions.TestingHelpers; using System.Linq; -using System.Text; using System.Threading.Tasks; using UpdateLib.Core.Storage; using UpdateLib.Core.Storage.Files; using Xunit; -using static System.Environment; namespace UpdateLib.Tests.Core.Storage { diff --git a/UpdateLib.Tests/Core/Storage/UpdateCatalogStorageTests.cs b/UpdateLib.Tests/Core/Storage/UpdateCatalogStorageTests.cs new file mode 100644 index 0000000..aa2e930 --- /dev/null +++ b/UpdateLib.Tests/Core/Storage/UpdateCatalogStorageTests.cs @@ -0,0 +1,37 @@ +using System.IO.Abstractions.TestingHelpers; +using System.Linq; +using System.Threading.Tasks; +using UpdateLib.Core.Common; +using UpdateLib.Core.Storage; +using UpdateLib.Core.Storage.Files; +using Xunit; + +namespace UpdateLib.Tests.Core.Storage +{ + public class UpdateCatalogStorageTests + { + [Fact] + public async Task SaveAndLoadAreTheSame() + { + var mockFileSystem = new MockFileSystem(); + + var storage = new UpdateCatalogStorage(mockFileSystem); + var file = new UpdateCatalogFile(); + var info = new UpdateInfo("1.0.0", null, "name", "hash"); + + file.Catalog.Add(info); + + await storage.SaveAsync(file); + + var loadedFile = await storage.LoadAsync(); + + var loadedEntry = loadedFile.Catalog.First(); + + Assert.Equal(info.FileName, loadedEntry.FileName); + Assert.Equal(info.Hash, loadedEntry.Hash); + Assert.Equal(info.IsPatch, loadedEntry.IsPatch); + Assert.Equal(info.Version, loadedEntry.Version); + Assert.Equal(info.BasedOnVersion, loadedEntry.BasedOnVersion); + } + } +} diff --git a/UpdateLib.Tests/Core/Storage/UpdateFileStorageTests.cs b/UpdateLib.Tests/Core/Storage/UpdateFileStorageTests.cs new file mode 100644 index 0000000..5865080 --- /dev/null +++ b/UpdateLib.Tests/Core/Storage/UpdateFileStorageTests.cs @@ -0,0 +1,39 @@ +using System.IO.Abstractions.TestingHelpers; +using System.Linq; +using System.Threading.Tasks; +using UpdateLib.Core.Common.IO; +using UpdateLib.Core.Storage; +using UpdateLib.Core.Storage.Files; +using Xunit; + +namespace UpdateLib.Tests.Core.Storage +{ + public class UpdateFileStorageTests + { + [Fact] + public async Task SaveAndLoadAreTheSame() + { + var mockFileSystem = new MockFileSystem(); + + var storage = new UpdateFileStorage(mockFileSystem); + var file = new UpdateFile(); + + var dir = new DirectoryEntry("dir"); + var fileEntry = new FileEntry("file.txt"); + + dir.Add(fileEntry); + + file.Entries.Add(dir); + + await storage.SaveAsync(file); + + var loadedFile = await storage.LoadAsync(); + + var loadedEntry = loadedFile.Entries.First().Files.First(); + + Assert.Equal(fileEntry.Hash, loadedEntry.Hash); + Assert.Equal(fileEntry.Name, loadedEntry.Name); + Assert.Equal(fileEntry.Path, loadedEntry.Path); + } + } +} diff --git a/UpdateLib/Core/Common/IO/DirectoryEntry.cs b/UpdateLib/Core/Common/IO/DirectoryEntry.cs index 1a66987..c06eaf6 100644 --- a/UpdateLib/Core/Common/IO/DirectoryEntry.cs +++ b/UpdateLib/Core/Common/IO/DirectoryEntry.cs @@ -1,73 +1,46 @@ -using System; +using Newtonsoft.Json; +using System; using System.Collections.Generic; using System.Linq; -using System.Text; namespace UpdateLib.Core.Common.IO { public class DirectoryEntry { - private readonly List directories = new List(); - private readonly List files = new List(); + [JsonProperty(PropertyName = "Directories")] + private List directories = new List(); + + [JsonProperty(PropertyName = "Files")] + private List files = new List(); public string Name { get; set; } + [JsonIgnore] public int Count => Files.Count + Directories.Sum(d => d.Count); - public IReadOnlyList Directories => directories.AsReadOnly(); - public IReadOnlyList Files => files.AsReadOnly(); - public DirectoryEntry Parent { get; set; } - - public string SourceLocation - { - get - { - StringBuilder sb = new StringBuilder(); - - if (Parent == null) - return string.Empty; - sb.Append(Parent.SourceLocation); - sb.Append(Name); - sb.Append(@"/"); - - return sb.ToString(); - } - } + [JsonIgnore] + public IReadOnlyList Directories => directories.AsReadOnly(); - public string DestinationLocation - { - get - { - StringBuilder sb = new StringBuilder(); + [JsonIgnore] + public IReadOnlyList Files => files.AsReadOnly(); - sb.Append(Parent?.DestinationLocation ?? string.Empty); - sb.Append(Name); - sb.Append(@"\"); + public string Path { get; set; } - return sb.ToString(); - } - } - - public DirectoryEntry() - { - } + public DirectoryEntry() { } public DirectoryEntry(string name) { Name = name ?? throw new ArgumentNullException(nameof(name)); - } - public DirectoryEntry(string name, DirectoryEntry parent) - { - Name = name ?? throw new ArgumentNullException(nameof(name)); - Parent = parent ?? throw new ArgumentNullException(nameof(parent)); + Path = $"{Name}\\"; } public void Add(DirectoryEntry folder) { if (folder == null) throw new ArgumentNullException(nameof(folder)); - folder.Parent = this; + folder.Path = $"{Path}{folder.Name}\\"; + directories.Add(folder); } @@ -75,24 +48,9 @@ public void Add(FileEntry file) { if (file == null) throw new ArgumentNullException(nameof(file)); - file.Parent = this; - files.Add(file); - } + file.Path = $"{Path}{file.Name}"; - public bool Remove(DirectoryEntry folder) - { - if (folder == null) throw new ArgumentNullException(nameof(folder)); - - folder.Parent = null; - return directories.Remove(folder); - } - - public bool Remove(FileEntry file) - { - if (file == null) throw new ArgumentNullException(nameof(file)); - - file.Parent = null; - return files.Remove(file); + files.Add(file); } /// diff --git a/UpdateLib/Core/Common/IO/FileEntry.cs b/UpdateLib/Core/Common/IO/FileEntry.cs index 309eef8..c551054 100644 --- a/UpdateLib/Core/Common/IO/FileEntry.cs +++ b/UpdateLib/Core/Common/IO/FileEntry.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; namespace UpdateLib.Core.Common.IO { @@ -10,8 +7,8 @@ public class FileEntry public string Hash { get; set; } public string Name { get; set; } - - public DirectoryEntry Parent { get; set; } + + public string Path { get; set; } public FileEntry() { } @@ -19,37 +16,5 @@ public FileEntry(string name) { Name = name ?? throw new ArgumentNullException(nameof(name)); } - - public FileEntry(string name, DirectoryEntry parent) - { - Name = name ?? throw new ArgumentNullException(nameof(name)); - Parent = parent ?? throw new ArgumentNullException(nameof(parent)); - } - - public string SourceLocation - { - get - { - StringBuilder sb = new StringBuilder(); - - sb.Append(Parent?.SourceLocation ?? string.Empty); - sb.Append(Name); - - return sb.ToString(); - } - } - - public string DestinationLocation - { - get - { - StringBuilder sb = new StringBuilder(); - - sb.Append(Parent?.DestinationLocation ?? string.Empty); - sb.Append(Name); - - return sb.ToString(); - } - } } } From eeb225a62f6f77534ccc72632d373a8db4efb61a Mon Sep 17 00:00:00 2001 From: Matthias Beerens <3512339+Matthiee@users.noreply.github.com> Date: Sun, 2 Feb 2020 19:52:59 +0100 Subject: [PATCH 14/14] Add CheckForUpdatesAsync tests --- UpdateLib.Tests/UpdaterTests.cs | 44 +++++++++++++++++++ .../Abstractions/IUpdateCatalogManager.cs | 4 ++ UpdateLib/Core/CheckForUpdatesResult.cs | 10 ++++- .../Core/Storage/Files/UpdateCatalogFile.cs | 12 ----- UpdateLib/Core/UpdateCatalogManager.cs | 15 +++++++ UpdateLib/Updater.cs | 10 ++++- 6 files changed, 80 insertions(+), 15 deletions(-) create mode 100644 UpdateLib.Tests/UpdaterTests.cs diff --git a/UpdateLib.Tests/UpdaterTests.cs b/UpdateLib.Tests/UpdaterTests.cs new file mode 100644 index 0000000..875a31e --- /dev/null +++ b/UpdateLib.Tests/UpdaterTests.cs @@ -0,0 +1,44 @@ +using Moq; +using System.Threading.Tasks; +using UpdateLib.Abstractions; +using Xunit; + +namespace UpdateLib.Tests +{ + public class UpdaterTests + { + [Fact] + public async Task InitializeShouldReturnTrueOnceInitialized() + { + var cacheManagerMock = new Mock(MockBehavior.Loose); + var catalogManagerMock = new Mock(MockBehavior.Loose); + + var updater = new Updater(cacheManagerMock.Object, catalogManagerMock.Object); + + await updater.InitializeAsync(); + + Assert.True(updater.IsInitialized); + } + + [Fact] + public async Task InitializeShouldReturnTrueAfterCheckForUpdates() + { + var cacheManagerMock = new Mock(MockBehavior.Loose); + var catalogManagerMock = new Mock(MockBehavior.Loose); + + cacheManagerMock + .Setup(_ => _.UpdateCacheAsync()) + .ReturnsAsync(new UpdateLib.Core.Storage.Files.HashCacheFile()); + + catalogManagerMock + .Setup(_ => _.GetUpdateCatalogFileAsync()) + .ReturnsAsync(new UpdateLib.Core.Storage.Files.UpdateCatalogFile()); + + var updater = new Updater(cacheManagerMock.Object, catalogManagerMock.Object); + + await updater.CheckForUpdatesAsync(); + + Assert.True(updater.IsInitialized); + } + } +} diff --git a/UpdateLib/Abstractions/IUpdateCatalogManager.cs b/UpdateLib/Abstractions/IUpdateCatalogManager.cs index 88f627e..968a0e1 100644 --- a/UpdateLib/Abstractions/IUpdateCatalogManager.cs +++ b/UpdateLib/Abstractions/IUpdateCatalogManager.cs @@ -1,4 +1,6 @@ using System.Threading.Tasks; +using UpdateLib.Core; +using UpdateLib.Core.Common; using UpdateLib.Core.Storage.Files; namespace UpdateLib.Abstractions @@ -6,5 +8,7 @@ namespace UpdateLib.Abstractions public interface IUpdateCatalogManager { Task GetUpdateCatalogFileAsync(); + + UpdateInfo GetLatestUpdateForVersion(UpdateVersion currentVersion, UpdateCatalogFile catalogFile); } } diff --git a/UpdateLib/Core/CheckForUpdatesResult.cs b/UpdateLib/Core/CheckForUpdatesResult.cs index a7ca87a..a5b6ee8 100644 --- a/UpdateLib/Core/CheckForUpdatesResult.cs +++ b/UpdateLib/Core/CheckForUpdatesResult.cs @@ -1,8 +1,16 @@ -namespace UpdateLib.Core +using UpdateLib.Core.Common; + +namespace UpdateLib.Core { public class CheckForUpdatesResult { public bool UpdateAvailable { get; private set; } public UpdateVersion NewVersion { get; private set; } + + public CheckForUpdatesResult(UpdateInfo info) + { + UpdateAvailable = info != null; + NewVersion = info?.Version; + } } } diff --git a/UpdateLib/Core/Storage/Files/UpdateCatalogFile.cs b/UpdateLib/Core/Storage/Files/UpdateCatalogFile.cs index 874a877..5d74029 100644 --- a/UpdateLib/Core/Storage/Files/UpdateCatalogFile.cs +++ b/UpdateLib/Core/Storage/Files/UpdateCatalogFile.cs @@ -16,17 +16,5 @@ public class UpdateCatalogFile /// Download Url's /// public List DownloadUrls { get; private set; } = new List(); - - /// - /// Gets the best update for the current version. - /// - /// The currect application version - /// - public UpdateInfo GetLatestUpdateForVersion(UpdateVersion currentVersion) - { - if (currentVersion == null) throw new ArgumentNullException(nameof(currentVersion)); - - return Catalog.OrderBy(c => c).Where(c => currentVersion < c.Version && ((c.IsPatch && c.BasedOnVersion == currentVersion) || !c.IsPatch)).FirstOrDefault(); - } } } diff --git a/UpdateLib/Core/UpdateCatalogManager.cs b/UpdateLib/Core/UpdateCatalogManager.cs index 2c22e5e..63ecd33 100644 --- a/UpdateLib/Core/UpdateCatalogManager.cs +++ b/UpdateLib/Core/UpdateCatalogManager.cs @@ -1,9 +1,11 @@ using Microsoft.Extensions.Logging; using System; +using System.Linq; using System.Threading.Tasks; using UpdateLib.Abstractions; using UpdateLib.Abstractions.Common.IO; using UpdateLib.Abstractions.Storage; +using UpdateLib.Core.Common; using UpdateLib.Core.Storage.Files; namespace UpdateLib.Core @@ -54,5 +56,18 @@ private async Task DownloadRemoteCatalogFileAsync() return file; } + + /// + /// Gets the best update for the current version. + /// + /// The currect application version + /// + public UpdateInfo GetLatestUpdateForVersion(UpdateVersion currentVersion, UpdateCatalogFile catalogFile) + { + if (currentVersion is null) throw new ArgumentNullException(nameof(currentVersion)); + if (catalogFile is null) throw new ArgumentNullException(nameof(catalogFile)); + + return catalogFile.Catalog.OrderBy(c => c).Where(c => currentVersion < c.Version && ((c.IsPatch && c.BasedOnVersion == currentVersion) || !c.IsPatch)).FirstOrDefault(); + } } } diff --git a/UpdateLib/Updater.cs b/UpdateLib/Updater.cs index fd8b9bf..9dcace0 100644 --- a/UpdateLib/Updater.cs +++ b/UpdateLib/Updater.cs @@ -9,13 +9,15 @@ namespace UpdateLib public class Updater : IUpdater { private readonly ICacheManager cacheManager; + private readonly IUpdateCatalogManager updateCatalogManager; private HashCacheFile cacheFile; public bool IsInitialized { get; private set; } - public Updater(ICacheManager cacheManager) + public Updater(ICacheManager cacheManager, IUpdateCatalogManager updateCatalogManager) { this.cacheManager = cacheManager ?? throw new ArgumentNullException(nameof(cacheManager)); + this.updateCatalogManager = updateCatalogManager ?? throw new ArgumentNullException(nameof(updateCatalogManager)); } public async Task CheckForUpdatesAsync() @@ -23,7 +25,11 @@ public async Task CheckForUpdatesAsync() if (!IsInitialized) await InitializeAsync(); - return null; + var catalogFile = await updateCatalogManager.GetUpdateCatalogFileAsync(); + + var updateInfo = updateCatalogManager.GetLatestUpdateForVersion(cacheFile.Version, catalogFile); + + return new CheckForUpdatesResult(updateInfo); } public async Task InitializeAsync()