diff --git a/UpdateLib.Tests/Core/CacheManagerTests.cs b/UpdateLib.Tests/Core/CacheManagerTests.cs new file mode 100644 index 0000000..dc5bb35 --- /dev/null +++ b/UpdateLib.Tests/Core/CacheManagerTests.cs @@ -0,0 +1,102 @@ +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(new Dictionary(), "C:\\app"); + 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(); + var cache = new CacheStorage(fs); + + fs.AddFile(cache.FileInfo.FullName, new MockFileData("blabla")); // not valid json + + 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/Core/Common/IO/DirectoryEntryTests.cs b/UpdateLib.Tests/Core/Common/IO/DirectoryEntryTests.cs new file mode 100644 index 0000000..d1a2100 --- /dev/null +++ b/UpdateLib.Tests/Core/Common/IO/DirectoryEntryTests.cs @@ -0,0 +1,44 @@ +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()); + } + + 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; + } + } +} diff --git a/UpdateLib.Tests/Core/Common/IO/FileEntryTests.cs b/UpdateLib.Tests/Core/Common/IO/FileEntryTests.cs new file mode 100644 index 0000000..8036c88 --- /dev/null +++ b/UpdateLib.Tests/Core/Common/IO/FileEntryTests.cs @@ -0,0 +1,24 @@ +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 outputDest = "%root%\\sub\\myfile.txt"; + + Assert.Equal(outputDest, file.Path); + } + } +} 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/Core/Storage/CacheStorageTests.cs b/UpdateLib.Tests/Core/Storage/CacheStorageTests.cs new file mode 100644 index 0000000..83b40b7 --- /dev/null +++ b/UpdateLib.Tests/Core/Storage/CacheStorageTests.cs @@ -0,0 +1,35 @@ +using System; +using System.IO.Abstractions.TestingHelpers; +using System.Linq; +using System.Threading.Tasks; +using UpdateLib.Core.Storage; +using UpdateLib.Core.Storage.Files; +using Xunit; + +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.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.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/Core/UpdateVersionTests.cs b/UpdateLib.Tests/Core/UpdateVersionTests.cs new file mode 100644 index 0000000..4ebbaaf --- /dev/null +++ b/UpdateLib.Tests/Core/UpdateVersionTests.cs @@ -0,0 +1,110 @@ +using System; +using UpdateLib.Core; +using UpdateLib.Core.Enums; +using Xunit; + +namespace UpdateLib.Tests.Core +{ + 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/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/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.Tests/UpdateLib.Tests.csproj b/UpdateLib.Tests/UpdateLib.Tests.csproj index 3e02510..229019c 100644 --- a/UpdateLib.Tests/UpdateLib.Tests.csproj +++ b/UpdateLib.Tests/UpdateLib.Tests.csproj @@ -1,4 +1,4 @@ - + netcoreapp2.2 @@ -7,9 +7,17 @@ + + + + + + + + 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/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/ICacheManager.cs b/UpdateLib/Abstractions/ICacheManager.cs new file mode 100644 index 0000000..dd90344 --- /dev/null +++ b/UpdateLib/Abstractions/ICacheManager.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using UpdateLib.Core.Storage.Files; + +namespace UpdateLib.Abstractions +{ + public interface ICacheManager + { + Task UpdateCacheAsync(); + } +} diff --git a/UpdateLib/Abstractions/IUpdateCatalogManager.cs b/UpdateLib/Abstractions/IUpdateCatalogManager.cs new file mode 100644 index 0000000..968a0e1 --- /dev/null +++ b/UpdateLib/Abstractions/IUpdateCatalogManager.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; +using UpdateLib.Core; +using UpdateLib.Core.Common; +using UpdateLib.Core.Storage.Files; + +namespace UpdateLib.Abstractions +{ + public interface IUpdateCatalogManager + { + Task GetUpdateCatalogFileAsync(); + + UpdateInfo GetLatestUpdateForVersion(UpdateVersion currentVersion, UpdateCatalogFile catalogFile); + } +} diff --git a/UpdateLib/Abstractions/IUpdater.cs b/UpdateLib/Abstractions/IUpdater.cs index 29e28d3..4d61da6 100644 --- a/UpdateLib/Abstractions/IUpdater.cs +++ b/UpdateLib/Abstractions/IUpdater.cs @@ -1,10 +1,13 @@ -using System; -using System.Collections.Generic; -using System.Text; + +using System.Threading.Tasks; +using UpdateLib.Core; namespace UpdateLib.Abstractions { interface IUpdater { + bool IsInitialized { get; } + Task CheckForUpdatesAsync(); + Task InitializeAsync(); } } 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 new file mode 100644 index 0000000..55d350c --- /dev/null +++ b/UpdateLib/Abstractions/Storage/ICacheStorage.cs @@ -0,0 +1,9 @@ +using UpdateLib.Core.Storage.Files; + +namespace UpdateLib.Abstractions.Storage +{ + public interface ICacheStorage : IStorage + { + bool CacheExists { get; } + } +} 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 new file mode 100644 index 0000000..cfeffab --- /dev/null +++ b/UpdateLib/Abstractions/Storage/IUpdateCatalogStorage.cs @@ -0,0 +1,11 @@ +using System; +using UpdateLib.Core.Storage.Files; + +namespace UpdateLib.Abstractions.Storage +{ + public interface IUpdateCatalogStorage : IStorage + { + bool Exists { get; } + DateTime LastWriteTime { get; } + } +} diff --git a/UpdateLib/Abstractions/Storage/IUpdateFileStorage.cs b/UpdateLib/Abstractions/Storage/IUpdateFileStorage.cs new file mode 100644 index 0000000..3863459 --- /dev/null +++ b/UpdateLib/Abstractions/Storage/IUpdateFileStorage.cs @@ -0,0 +1,8 @@ +using UpdateLib.Core.Storage.Files; + +namespace UpdateLib.Abstractions.Storage +{ + interface IUpdateFileStorage : IStorage + { + } +} diff --git a/UpdateLib/Core/CacheManager.cs b/UpdateLib/Core/CacheManager.cs new file mode 100644 index 0000000..7ed752a --- /dev/null +++ b/UpdateLib/Core/CacheManager.cs @@ -0,0 +1,145 @@ +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; + + if (cacheStorage.CacheExists) + { + try + { + file = await cacheStorage.LoadAsync(); + } + catch (Exception e) + { + logger.LogError(e, "Unable to load cache from storage"); + } + } + else + { + logger.LogDebug($"Cache file doesn't exist"); + } + + 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 file; + } + + await UpdateExistingFiles(file); + + await cacheStorage.SaveAsync(file); + + return 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 new file mode 100644 index 0000000..a5b6ee8 --- /dev/null +++ b/UpdateLib/Core/CheckForUpdatesResult.cs @@ -0,0 +1,16 @@ +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/Common/IO/DirectoryEntry.cs b/UpdateLib/Core/Common/IO/DirectoryEntry.cs new file mode 100644 index 0000000..c06eaf6 --- /dev/null +++ b/UpdateLib/Core/Common/IO/DirectoryEntry.cs @@ -0,0 +1,65 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace UpdateLib.Core.Common.IO +{ + public class DirectoryEntry + { + [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); + + [JsonIgnore] + public IReadOnlyList Directories => directories.AsReadOnly(); + + [JsonIgnore] + public IReadOnlyList Files => files.AsReadOnly(); + + public string Path { get; set; } + + public DirectoryEntry() { } + + public DirectoryEntry(string name) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + + Path = $"{Name}\\"; + } + + public void Add(DirectoryEntry folder) + { + if (folder == null) throw new ArgumentNullException(nameof(folder)); + + folder.Path = $"{Path}{folder.Name}\\"; + + directories.Add(folder); + } + + public void Add(FileEntry file) + { + if (file == null) throw new ArgumentNullException(nameof(file)); + + file.Path = $"{Path}{file.Name}"; + + files.Add(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/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/Common/IO/FileEntry.cs b/UpdateLib/Core/Common/IO/FileEntry.cs new file mode 100644 index 0000000..c551054 --- /dev/null +++ b/UpdateLib/Core/Common/IO/FileEntry.cs @@ -0,0 +1,20 @@ +using System; + +namespace UpdateLib.Core.Common.IO +{ + public class FileEntry + { + public string Hash { get; set; } + + public string Name { get; set; } + + public string Path { get; set; } + + public FileEntry() { } + + public FileEntry(string name) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + } + } +} 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); + } +} 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/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..e71811e --- /dev/null +++ b/UpdateLib/Core/Storage/CacheStorage.cs @@ -0,0 +1,19 @@ +using System.IO.Abstractions; +using UpdateLib.Abstractions.Storage; +using UpdateLib.Core.Storage.Files; + +namespace UpdateLib.Core.Storage +{ + public class CacheStorage : BaseStorage, ICacheStorage + { + private const string CachePathName = "Cache"; + private const string CacheFileName = "FileCache.json"; + + public bool CacheExists => FileInfo.Exists; + + public CacheStorage(IFileSystem storage) + : base(storage, "UpdateLib", CachePathName, CacheFileName) + { + } + } +} 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..7ea9792 --- /dev/null +++ b/UpdateLib/Core/Storage/Files/HashCacheFile.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; + +namespace UpdateLib.Core.Storage.Files +{ + public class HashCacheFile + { + public UpdateVersion Version { get; set; } + public List Entries { get; set; } + + public HashCacheFile() + { + Entries = new List(); + } + } +} diff --git a/UpdateLib/Core/Storage/Files/UpdateCatalogFile.cs b/UpdateLib/Core/Storage/Files/UpdateCatalogFile.cs new file mode 100644 index 0000000..5d74029 --- /dev/null +++ b/UpdateLib/Core/Storage/Files/UpdateCatalogFile.cs @@ -0,0 +1,20 @@ +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(); + } +} 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/UpdateCatalogStorage.cs b/UpdateLib/Core/Storage/UpdateCatalogStorage.cs new file mode 100644 index 0000000..179be87 --- /dev/null +++ b/UpdateLib/Core/Storage/UpdateCatalogStorage.cs @@ -0,0 +1,21 @@ +using System; +using System.IO.Abstractions; +using UpdateLib.Abstractions.Storage; +using UpdateLib.Core.Storage.Files; + +namespace UpdateLib.Core.Storage +{ + public class UpdateCatalogStorage : BaseStorage, IUpdateCatalogStorage + { + private const string FileName = "UpdateCatalogus.json"; + + public bool Exists => FileInfo.Exists; + + public DateTime LastWriteTime => FileInfo.LastWriteTimeUtc; + + public UpdateCatalogStorage(IFileSystem fs) + : base(fs, "UpdateLib", "Cache", FileName) + { + } + } +} diff --git a/UpdateLib/Core/Storage/UpdateFileStorage.cs b/UpdateLib/Core/Storage/UpdateFileStorage.cs new file mode 100644 index 0000000..73bd33b --- /dev/null +++ b/UpdateLib/Core/Storage/UpdateFileStorage.cs @@ -0,0 +1,16 @@ +using System.IO.Abstractions; +using UpdateLib.Abstractions.Storage; +using UpdateLib.Core.Storage.Files; + +namespace UpdateLib.Core.Storage +{ + public class UpdateFileStorage : BaseStorage, IUpdateFileStorage + { + private const string UpdateFileName = "UpdateInfo.json"; + + public UpdateFileStorage(IFileSystem fs) + : base(fs, "UpdateLib", string.Empty, UpdateFileName) + { + } + } +} diff --git a/UpdateLib/Core/UpdateCatalogManager.cs b/UpdateLib/Core/UpdateCatalogManager.cs new file mode 100644 index 0000000..63ecd33 --- /dev/null +++ b/UpdateLib/Core/UpdateCatalogManager.cs @@ -0,0 +1,73 @@ +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 +{ + 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) + { + return await DownloadRemoteCatalogFileAsync(); + } + + 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); + + 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/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..fe85853 100644 --- a/UpdateLib/UpdateLib.csproj +++ b/UpdateLib/UpdateLib.csproj @@ -1,15 +1,16 @@ - + netstandard2.0 - - - - + + + + + diff --git a/UpdateLib/Updater.cs b/UpdateLib/Updater.cs index deb6eab..9dcace0 100644 --- a/UpdateLib/Updater.cs +++ b/UpdateLib/Updater.cs @@ -1,8 +1,42 @@ -using UpdateLib.Abstractions; +using System; +using System.Threading.Tasks; +using UpdateLib.Abstractions; +using UpdateLib.Core; +using UpdateLib.Core.Storage.Files; 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, IUpdateCatalogManager updateCatalogManager) + { + this.cacheManager = cacheManager ?? throw new ArgumentNullException(nameof(cacheManager)); + this.updateCatalogManager = updateCatalogManager ?? throw new ArgumentNullException(nameof(updateCatalogManager)); + } + + public async Task CheckForUpdatesAsync() + { + if (!IsInitialized) + await InitializeAsync(); + + var catalogFile = await updateCatalogManager.GetUpdateCatalogFileAsync(); + + var updateInfo = updateCatalogManager.GetLatestUpdateForVersion(cacheFile.Version, catalogFile); + + return new CheckForUpdatesResult(updateInfo); + } + + public async Task InitializeAsync() + { + cacheFile = await cacheManager.UpdateCacheAsync(); + + IsInitialized = true; + } } }