Implement an archive verification cache

This commit is contained in:
Halgari 2022-10-17 22:48:49 -06:00
parent 320df0d96d
commit 4210234fe5
11 changed files with 218 additions and 27 deletions

View File

@ -0,0 +1,43 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Wabbajack.Paths.IO;
using Xunit;
namespace Wabbajack.Downloaders.Dispatcher.Test;
public class VerificationCacheTests
{
private readonly TemporaryFileManager _temp;
private readonly ILogger<VerificationCache.VerificationCache> _logger;
public VerificationCacheTests(ILogger<VerificationCache.VerificationCache> logger)
{
_logger = logger;
}
[Fact]
public async Task BasicCacheTests()
{
using var cache = new VerificationCache.VerificationCache(_logger, KnownFolders.EntryPoint.Combine(Guid.NewGuid().ToString()), TimeSpan.FromSeconds(1));
var goodState = new DTOs.DownloadStates.Http { Url = new Uri($"https://some.com/{Guid.NewGuid()}/path") };
var badState = new DTOs.DownloadStates.Http { Url = new Uri($"https://some.com/{Guid.NewGuid()}/path") };
Assert.True(await cache.Get(goodState) == null);
await cache.Put(goodState, true);
Assert.True(await cache.Get(goodState));
await Task.Delay(TimeSpan.FromSeconds(2));
Assert.False(await cache.Get(goodState));
await cache.Put(badState, true);
Assert.True(await cache.Get(badState));
await cache.Put(badState, false);
Assert.Null(await cache.Get(badState));
}
}

View File

@ -27,6 +27,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Wabbajack.Downloaders.Dispatcher\Wabbajack.Downloaders.Dispatcher.csproj" /> <ProjectReference Include="..\Wabbajack.Downloaders.Dispatcher\Wabbajack.Downloaders.Dispatcher.csproj" />
<ProjectReference Include="..\Wabbajack.Downloaders.VerificationCache\Wabbajack.Downloaders.VerificationCache.csproj" />
<ProjectReference Include="..\Wabbajack.Networking.Http\Wabbajack.Networking.Http.csproj" /> <ProjectReference Include="..\Wabbajack.Networking.Http\Wabbajack.Networking.Http.csproj" />
<ProjectReference Include="..\Wabbajack.Networking.NexusApi.Test\Wabbajack.Networking.NexusApi.Test.csproj" /> <ProjectReference Include="..\Wabbajack.Networking.NexusApi.Test\Wabbajack.Networking.NexusApi.Test.csproj" />
<ProjectReference Include="..\Wabbajack.Networking.NexusApi\Wabbajack.Networking.NexusApi.csproj" /> <ProjectReference Include="..\Wabbajack.Networking.NexusApi\Wabbajack.Networking.NexusApi.csproj" />

View File

@ -6,6 +6,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Wabbajack.Downloaders.Interfaces; using Wabbajack.Downloaders.Interfaces;
using Wabbajack.Downloaders.VerificationCache;
using Wabbajack.DTOs; using Wabbajack.DTOs;
using Wabbajack.DTOs.DownloadStates; using Wabbajack.DTOs.DownloadStates;
using Wabbajack.DTOs.ServerResponses; using Wabbajack.DTOs.ServerResponses;
@ -16,6 +17,7 @@ using Wabbajack.Networking.WabbajackClientApi;
using Wabbajack.Paths; using Wabbajack.Paths;
using Wabbajack.Paths.IO; using Wabbajack.Paths.IO;
using Wabbajack.RateLimiter; using Wabbajack.RateLimiter;
using StringExtensions = Wabbajack.Paths.StringExtensions;
namespace Wabbajack.Downloaders; namespace Wabbajack.Downloaders;
@ -26,15 +28,17 @@ public class DownloadDispatcher
private readonly ILogger<DownloadDispatcher> _logger; private readonly ILogger<DownloadDispatcher> _logger;
private readonly Client _wjClient; private readonly Client _wjClient;
private readonly bool _useProxyCache; private readonly bool _useProxyCache;
private readonly IVerificationCache _verificationCache;
public DownloadDispatcher(ILogger<DownloadDispatcher> logger, IEnumerable<IDownloader> downloaders, public DownloadDispatcher(ILogger<DownloadDispatcher> logger, IEnumerable<IDownloader> downloaders,
IResource<DownloadDispatcher> limiter, Client wjClient, bool useProxyCache = true) IResource<DownloadDispatcher> limiter, Client wjClient, IVerificationCache verificationCache, bool useProxyCache = true)
{ {
_downloaders = downloaders.OrderBy(d => d.Priority).ToArray(); _downloaders = downloaders.OrderBy(d => d.Priority).ToArray();
_logger = logger; _logger = logger;
_wjClient = wjClient; _wjClient = wjClient;
_limiter = limiter; _limiter = limiter;
_useProxyCache = useProxyCache; _useProxyCache = useProxyCache;
_verificationCache = verificationCache;
} }
public async Task<Hash> Download(Archive a, AbsolutePath dest, CancellationToken token, bool? proxy = null) public async Task<Hash> Download(Archive a, AbsolutePath dest, CancellationToken token, bool? proxy = null)
@ -108,13 +112,20 @@ public class DownloadDispatcher
{ {
try try
{ {
if (await _verificationCache.Get(a.State) == true)
return true;
a = await MaybeProxy(a, token); a = await MaybeProxy(a, token);
var downloader = Downloader(a); var downloader = Downloader(a);
using var job = await _limiter.Begin($"Verifying {a.State.PrimaryKeyString}", -1, token); using var job = await _limiter.Begin($"Verifying {a.State.PrimaryKeyString}", -1, token);
return await downloader.Verify(a, job, token); var result = await downloader.Verify(a, job, token);
await _verificationCache.Put(a.State, result);
return result;
} }
catch (HttpException) catch (HttpException)
{ {
await _verificationCache.Put(a.State, false);
return false; return false;
} }
} }

View File

@ -8,6 +8,7 @@ using Wabbajack.Downloaders.IPS4OAuth2Downloader;
using Wabbajack.Downloaders.Manual; using Wabbajack.Downloaders.Manual;
using Wabbajack.Downloaders.MediaFire; using Wabbajack.Downloaders.MediaFire;
using Wabbajack.Downloaders.ModDB; using Wabbajack.Downloaders.ModDB;
using Wabbajack.Downloaders.VerificationCache;
using Wabbajack.DTOs.JsonConverters; using Wabbajack.DTOs.JsonConverters;
using Wabbajack.Networking.WabbajackClientApi; using Wabbajack.Networking.WabbajackClientApi;
using Wabbajack.RateLimiter; using Wabbajack.RateLimiter;
@ -55,6 +56,7 @@ public static class ServiceExtensions
s.GetRequiredService<IEnumerable<IDownloader>>(), s.GetRequiredService<IEnumerable<IDownloader>>(),
s.GetRequiredService<IResource<DownloadDispatcher>>(), s.GetRequiredService<IResource<DownloadDispatcher>>(),
s.GetRequiredService<Client>(), s.GetRequiredService<Client>(),
s.GetRequiredService<IVerificationCache>(),
useProxyCache)); useProxyCache));
return services; return services;

View File

@ -19,6 +19,7 @@
<ProjectReference Include="..\Wabbajack.Downloaders.Mega\Wabbajack.Downloaders.Mega.csproj" /> <ProjectReference Include="..\Wabbajack.Downloaders.Mega\Wabbajack.Downloaders.Mega.csproj" />
<ProjectReference Include="..\Wabbajack.Downloaders.ModDB\Wabbajack.Downloaders.ModDB.csproj" /> <ProjectReference Include="..\Wabbajack.Downloaders.ModDB\Wabbajack.Downloaders.ModDB.csproj" />
<ProjectReference Include="..\Wabbajack.Downloaders.Nexus\Wabbajack.Downloaders.Nexus.csproj" /> <ProjectReference Include="..\Wabbajack.Downloaders.Nexus\Wabbajack.Downloaders.Nexus.csproj" />
<ProjectReference Include="..\Wabbajack.Downloaders.VerificationCache\Wabbajack.Downloaders.VerificationCache.csproj" />
<ProjectReference Include="..\Wabbajack.Downloaders.WabbajackCDN\Wabbajack.Downloaders.WabbajackCDN.csproj" /> <ProjectReference Include="..\Wabbajack.Downloaders.WabbajackCDN\Wabbajack.Downloaders.WabbajackCDN.csproj" />
<ProjectReference Include="..\Wabbajack.Networking.WabbajackClientApi\Wabbajack.Networking.WabbajackClientApi.csproj" /> <ProjectReference Include="..\Wabbajack.Networking.WabbajackClientApi\Wabbajack.Networking.WabbajackClientApi.csproj" />
</ItemGroup> </ItemGroup>

View File

@ -139,31 +139,31 @@ public class GameLocator : IGameLocator
{ {
var metaData = game.MetaData(); var metaData = game.MetaData();
int? steamId = metaData.SteamIDs.FirstOrDefault(id => _steamGames.ContainsKey(id)); foreach (var id in metaData.SteamIDs)
if (steamId.HasValue)
{ {
path = _steamGames[steamId.Value]; if (!_steamGames.TryGetValue(id, out var found)) continue;
path = found;
return true; return true;
} }
int? gogId = metaData.GOGIDs.FirstOrDefault(id => _gogGames.ContainsKey(id)); foreach (var id in metaData.GOGIDs)
if (gogId.HasValue)
{ {
path = _gogGames[gogId.Value]; if (!_gogGames.TryGetValue(id, out var found)) continue;
path = found;
return true; return true;
} }
var egsId = metaData.EpicGameStoreIDs.FirstOrDefault(id => _egsGames.ContainsKey(id)); foreach (var id in metaData.EpicGameStoreIDs)
if (egsId is not null)
{ {
path = _egsGames[egsId]; if (!_egsGames.TryGetValue(id, out var found)) continue;
path = found;
return true; return true;
} }
var originId = metaData.OriginIDs.FirstOrDefault(id => _originGames.ContainsKey(id)); foreach (var id in metaData.OriginIDs)
if (originId is not null)
{ {
path = _originGames[originId]; if (!_originGames.TryGetValue(id, out var found)) continue;
path = found;
return true; return true;
} }

View File

@ -0,0 +1,10 @@
using Wabbajack.DTOs;
using Wabbajack.DTOs.DownloadStates;
namespace Wabbajack.Downloaders.VerificationCache;
public interface IVerificationCache
{
Task<bool?> Get(IDownloadState archive);
Task Put(IDownloadState archive, bool valid);
}

View File

@ -0,0 +1,91 @@
using System.Data.SQLite;
using Microsoft.Extensions.Logging;
using Wabbajack.DTOs;
using Wabbajack.DTOs.DownloadStates;
using Wabbajack.Hashing.xxHash64;
using Wabbajack.Paths;
using Wabbajack.Paths.IO;
namespace Wabbajack.Downloaders.VerificationCache;
public class VerificationCache : IVerificationCache, IDisposable
{
private readonly AbsolutePath _location;
private readonly string _connectionString;
private readonly SQLiteConnection _conn;
private readonly TimeSpan _expiry;
private readonly ILogger<VerificationCache> _logger;
public VerificationCache(ILogger<VerificationCache> logger, AbsolutePath location, TimeSpan expiry)
{
_logger = logger;
_location = location;
_expiry = expiry;
if (!_location.Parent.DirectoryExists())
_location.Parent.CreateDirectory();
_connectionString =
string.Intern($"URI=file:{_location};Pooling=True;Max Pool Size=100; Journal Mode=Memory;");
_conn = new SQLiteConnection(_connectionString);
_conn.Open();
using var cmd = new SQLiteCommand(_conn);
cmd.CommandText = @"CREATE TABLE IF NOT EXISTS VerficationCache (
PKS TEXT PRIMARY KEY,
LastModified BIGINT)
WITHOUT ROWID";
cmd.ExecuteNonQuery();
}
public async Task<bool?> Get(IDownloadState archive)
{
var key = archive.PrimaryKeyString;
await using var cmd = new SQLiteCommand(_conn);
cmd.CommandText = "SELECT LastModified FROM VerficationCache WHERE PKS = @pks";
cmd.Parameters.AddWithValue("@pks", key);
await cmd.PrepareAsync();
await using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
var ts = DateTime.FromFileTimeUtc(reader.GetInt64(0));
return DateTime.UtcNow - ts <= _expiry;
}
return null;
}
public async Task Put(IDownloadState state, bool valid)
{
var key = state.PrimaryKeyString;
if (valid)
{
await using var cmd = new SQLiteCommand(_conn);
cmd.CommandText = @"INSERT INTO VerficationCache (PKS, LastModified) VALUES (@pks, @lastModified)
ON CONFLICT(PKS) DO UPDATE SET LastModified = @lastModified";
cmd.Parameters.AddWithValue("@pks", key);
cmd.Parameters.AddWithValue("@lastModified", DateTime.UtcNow.ToFileTimeUtc());
await cmd.PrepareAsync();
await cmd.ExecuteNonQueryAsync();
}
else
{
_logger.LogInformation("Marking {Key} as invalid", key);
await using var cmd = new SQLiteCommand(_conn);
cmd.CommandText = @"DELETE FROM VerficationCache WHERE PKS = @pks";
cmd.Parameters.AddWithValue("@pks", state.PrimaryKeyString);
await cmd.PrepareAsync();
await cmd.ExecuteNonQueryAsync();
}
}
public void Dispose()
{
_conn.Close();
_conn.Dispose();
}
}

View File

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Wabbajack.DTOs\Wabbajack.DTOs.csproj" />
<ProjectReference Include="..\Wabbajack.Paths.IO\Wabbajack.Paths.IO.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.2-mauipre.1.22102.15" />
<PackageReference Include="Stub.System.Data.SQLite.Core.NetStandard" Version="1.0.116" />
</ItemGroup>
</Project>

View File

@ -10,6 +10,7 @@ using Microsoft.Extensions.Logging;
using Wabbajack.Compiler; using Wabbajack.Compiler;
using Wabbajack.Downloaders; using Wabbajack.Downloaders;
using Wabbajack.Downloaders.GameFile; using Wabbajack.Downloaders.GameFile;
using Wabbajack.Downloaders.VerificationCache;
using Wabbajack.DTOs; using Wabbajack.DTOs;
using Wabbajack.DTOs.Interventions; using Wabbajack.DTOs.Interventions;
using Wabbajack.DTOs.Logins; using Wabbajack.DTOs.Logins;
@ -71,6 +72,11 @@ public static class ServiceExtensions
? new BinaryPatchCache(s.GetRequiredService<ILogger<BinaryPatchCache>>(), s.GetService<TemporaryFileManager>()!.CreateFolder().Path) ? new BinaryPatchCache(s.GetRequiredService<ILogger<BinaryPatchCache>>(), s.GetService<TemporaryFileManager>()!.CreateFolder().Path)
: new BinaryPatchCache(s.GetRequiredService<ILogger<BinaryPatchCache>>(),KnownFolders.WabbajackAppLocal.Combine("PatchCache"))); : new BinaryPatchCache(s.GetRequiredService<ILogger<BinaryPatchCache>>(),KnownFolders.WabbajackAppLocal.Combine("PatchCache")));
service.AddSingleton<IVerificationCache>(s => options.UseLocalCache
? new VerificationCache(s.GetRequiredService<ILogger<VerificationCache>>(), s.GetService<TemporaryFileManager>()!.CreateFile().Path, TimeSpan.FromDays(1))
: new VerificationCache(s.GetRequiredService<ILogger<VerificationCache>>(),KnownFolders.WabbajackAppLocal.Combine("VerificationCache"), TimeSpan.FromDays(1)));
service.AddSingleton(new ParallelOptions {MaxDegreeOfParallelism = Environment.ProcessorCount}); service.AddSingleton(new ParallelOptions {MaxDegreeOfParallelism = Environment.ProcessorCount});
Func<Task<(int MaxTasks, long MaxThroughput)>> GetSettings(IServiceProvider provider, string name) Func<Task<(int MaxTasks, long MaxThroughput)>> GetSettings(IServiceProvider provider, string name)

View File

@ -143,6 +143,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wabbajack.VFS.Interfaces",
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wabbajack.CLI.Builder", "Wabbajack.CLI.Builder\Wabbajack.CLI.Builder.csproj", "{99CD51A1-38C9-420E-9497-2C9AEAF0053C}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wabbajack.CLI.Builder", "Wabbajack.CLI.Builder\Wabbajack.CLI.Builder.csproj", "{99CD51A1-38C9-420E-9497-2C9AEAF0053C}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wabbajack.Downloaders.VerificationCache", "Wabbajack.Downloaders.VerificationCache\Wabbajack.Downloaders.VerificationCache.csproj", "{D9560C73-4E58-4463-9DB9-D06491E0E1C8}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -393,6 +395,10 @@ Global
{99CD51A1-38C9-420E-9497-2C9AEAF0053C}.Debug|Any CPU.Build.0 = Debug|Any CPU {99CD51A1-38C9-420E-9497-2C9AEAF0053C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{99CD51A1-38C9-420E-9497-2C9AEAF0053C}.Release|Any CPU.ActiveCfg = Release|Any CPU {99CD51A1-38C9-420E-9497-2C9AEAF0053C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{99CD51A1-38C9-420E-9497-2C9AEAF0053C}.Release|Any CPU.Build.0 = Release|Any CPU {99CD51A1-38C9-420E-9497-2C9AEAF0053C}.Release|Any CPU.Build.0 = Release|Any CPU
{D9560C73-4E58-4463-9DB9-D06491E0E1C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D9560C73-4E58-4463-9DB9-D06491E0E1C8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D9560C73-4E58-4463-9DB9-D06491E0E1C8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D9560C73-4E58-4463-9DB9-D06491E0E1C8}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -442,6 +448,7 @@ Global
{B10BB6D6-B3FC-4A76-8A07-6A0A0ADDE198} = {98B731EE-4FC0-4482-A069-BCBA25497871} {B10BB6D6-B3FC-4A76-8A07-6A0A0ADDE198} = {98B731EE-4FC0-4482-A069-BCBA25497871}
{7FC4F129-F0FA-46B7-B7C4-532E371A6326} = {98B731EE-4FC0-4482-A069-BCBA25497871} {7FC4F129-F0FA-46B7-B7C4-532E371A6326} = {98B731EE-4FC0-4482-A069-BCBA25497871}
{E4BDB22D-11A4-452F-8D10-D9CA9777EA22} = {F677890D-5109-43BC-97C7-C4CD47C8EE0C} {E4BDB22D-11A4-452F-8D10-D9CA9777EA22} = {F677890D-5109-43BC-97C7-C4CD47C8EE0C}
{D9560C73-4E58-4463-9DB9-D06491E0E1C8} = {98B731EE-4FC0-4482-A069-BCBA25497871}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {0AA30275-0F38-4A7D-B645-F5505178DDE8} SolutionGuid = {0AA30275-0F38-4A7D-B645-F5505178DDE8}