mirror of
https://github.com/wabbajack-tools/wabbajack.git
synced 2024-08-30 18:42:17 +00:00
Merge pull request #1973 from wabbajack-tools/cesi-wip
Add CESI support, start integration into compiler
This commit is contained in:
@ -11,7 +11,7 @@ public class ManualBlobDownloadHandler : BrowserWindowViewModel
|
||||
|
||||
protected override async Task Run(CancellationToken token)
|
||||
{
|
||||
await WaitForReady();
|
||||
//await WaitForReady();
|
||||
var archive = Intervention.Archive;
|
||||
var md = Intervention.Archive.State as Manual;
|
||||
|
||||
|
@ -135,6 +135,9 @@ public abstract class BrowserWindowViewModel : ViewModel
|
||||
{
|
||||
var source = new TaskCompletionSource();
|
||||
var referer = _browser.Source;
|
||||
while (_browser.CoreWebView2 == null)
|
||||
await Task.Delay(10, token);
|
||||
|
||||
_browser.CoreWebView2.DownloadStarting += (sender, args) =>
|
||||
{
|
||||
try
|
||||
|
@ -47,7 +47,6 @@ internal class Program
|
||||
services.AddSingleton<CommandLineBuilder, CommandLineBuilder>();
|
||||
services.AddSingleton<TemporaryFileManager>();
|
||||
services.AddSingleton<FileExtractor.FileExtractor>();
|
||||
services.AddSingleton(new VFSCache(KnownFolders.EntryPoint.Combine("vfscache.sqlite")));
|
||||
services.AddSingleton(new ParallelOptions {MaxDegreeOfParallelism = Environment.ProcessorCount});
|
||||
services.AddSingleton<Client>();
|
||||
services.AddSingleton<Networking.WabbajackClientApi.Client>();
|
||||
@ -79,6 +78,7 @@ internal class Program
|
||||
services.AddSingleton<IVerb, Install>();
|
||||
services.AddSingleton<IVerb, InstallCompileInstallVerify>();
|
||||
services.AddSingleton<IVerb, HashUrlString>();
|
||||
services.AddSingleton<IVerb, DownloadAll>();
|
||||
|
||||
services.AddSingleton<IUserInterventionHandler, UserInterventionHandler>();
|
||||
}).Build();
|
||||
|
128
Wabbajack.CLI/Verbs/DownloadAll.cs
Normal file
128
Wabbajack.CLI/Verbs/DownloadAll.cs
Normal file
@ -0,0 +1,128 @@
|
||||
using System;
|
||||
using System.CommandLine;
|
||||
using System.CommandLine.Invocation;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Wabbajack.Common;
|
||||
using Wabbajack.Downloaders;
|
||||
using Wabbajack.DTOs;
|
||||
using Wabbajack.DTOs.DownloadStates;
|
||||
using Wabbajack.DTOs.JsonConverters;
|
||||
using Wabbajack.Installer;
|
||||
using Wabbajack.Networking.WabbajackClientApi;
|
||||
using Wabbajack.Paths;
|
||||
using Wabbajack.Paths.IO;
|
||||
using Wabbajack.RateLimiter;
|
||||
using Wabbajack.VFS;
|
||||
|
||||
namespace Wabbajack.CLI.Verbs;
|
||||
|
||||
public class DownloadAll : IVerb
|
||||
{
|
||||
private readonly DownloadDispatcher _dispatcher;
|
||||
private readonly ILogger<DownloadAll> _logger;
|
||||
private readonly Client _wjClient;
|
||||
private readonly DTOSerializer _dtos;
|
||||
private readonly Resource<DownloadAll> _limiter;
|
||||
private readonly FileHashCache _cache;
|
||||
|
||||
public const int MaxDownload = 6000;
|
||||
|
||||
public DownloadAll(ILogger<DownloadAll> logger, DownloadDispatcher dispatcher, Client wjClient, DTOSerializer dtos, FileHashCache cache)
|
||||
{
|
||||
_logger = logger;
|
||||
_dispatcher = dispatcher;
|
||||
_wjClient = wjClient;
|
||||
_dtos = dtos;
|
||||
_limiter = new Resource<DownloadAll>("Download All", 16);
|
||||
_cache = cache;
|
||||
}
|
||||
|
||||
public Command MakeCommand()
|
||||
{
|
||||
var command = new Command("download-all");
|
||||
command.Add(new Option<AbsolutePath>(new[] {"-o", "-output"}, "Output folder"));
|
||||
command.Description = "Downloads all files for all modlists in the gallery";
|
||||
command.Handler = CommandHandler.Create(Run);
|
||||
return command;
|
||||
}
|
||||
|
||||
private async Task<int> Run(AbsolutePath output, CancellationToken token)
|
||||
{
|
||||
_logger.LogInformation("Downloading modlists");
|
||||
|
||||
var existing = await output.EnumerateFiles()
|
||||
.Where(f => f.Extension != Ext.Meta)
|
||||
.PMapAll(_limiter, async f =>
|
||||
{
|
||||
_logger.LogInformation("Hashing {File}", f.FileName);
|
||||
return await _cache.FileHashCachedAsync(f, token);
|
||||
})
|
||||
.ToHashSet();
|
||||
|
||||
var archives = (await (await _wjClient.LoadLists())
|
||||
.PMapAll(_limiter, async m =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return await StandardInstaller.Load(_dtos, _dispatcher, m, token);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "While downloading list");
|
||||
return default;
|
||||
}
|
||||
})
|
||||
.Where(d => d != default)
|
||||
.SelectMany(m => m!.Archives)
|
||||
.ToList())
|
||||
.DistinctBy(d => d.Hash)
|
||||
.Where(d => d.State is Nexus)
|
||||
.Where(d => !existing.Contains(d.Hash))
|
||||
.ToList();
|
||||
|
||||
|
||||
|
||||
_logger.LogInformation("Found {Count} Archives totaling {Size}", archives.Count, archives.Sum(a => a.Size).ToFileSizeString());
|
||||
|
||||
await archives
|
||||
.OrderBy(a => a.Size)
|
||||
.Take(MaxDownload)
|
||||
.PDoAll(_limiter, async file => {
|
||||
var outputFile = output.Combine(file.Name);
|
||||
if (outputFile.FileExists())
|
||||
{
|
||||
outputFile = output.Combine(outputFile.FileName.WithoutExtension() + "_" + file.Hash.ToHex() +
|
||||
outputFile.WithExtension(outputFile.Extension));
|
||||
}
|
||||
|
||||
_logger.LogInformation("Downloading {File}", file.Name);
|
||||
|
||||
try
|
||||
{
|
||||
var result = await _dispatcher.DownloadWithPossibleUpgrade(file, outputFile, token);
|
||||
if (result.Item1 == DownloadResult.Failure)
|
||||
{
|
||||
if (outputFile.FileExists())
|
||||
outputFile.Delete();
|
||||
return;
|
||||
}
|
||||
|
||||
_cache.FileHashWriteCache(output, result.Item2);
|
||||
|
||||
var metaFile = outputFile.WithExtension(Ext.Meta);
|
||||
await metaFile.WriteAllTextAsync(_dispatcher.MetaIniSection(file), token: token);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "While downloading {Name}, Ignoring", file.Name);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
@ -30,6 +30,7 @@ public static class AsyncParallelExtensions
|
||||
foreach (var itm in tasks) yield return await itm;
|
||||
}
|
||||
|
||||
|
||||
// Like PMapAll but don't keep defaults
|
||||
public static async IAsyncEnumerable<TOut> PKeepAll<TIn, TOut>(this IEnumerable<TIn> coll,
|
||||
Func<TIn, Task<TOut>> mapFn)
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Wabbajack.Paths;
|
||||
|
||||
namespace Wabbajack.DTOs.JsonConverters;
|
||||
|
||||
@ -18,6 +19,7 @@ public static class DIExtensions
|
||||
services.AddSingleton<JsonConverter, RelativePathConverter>();
|
||||
services.AddSingleton<JsonConverter, AbsolutePathConverter>();
|
||||
services.AddSingleton<JsonConverter, VersionConverter>();
|
||||
services.AddSingleton<JsonConverter, IPathConverter>();
|
||||
return services;
|
||||
}
|
||||
|
||||
|
45
Wabbajack.DTOs/JsonConverters/IPathConverter.cs
Normal file
45
Wabbajack.DTOs/JsonConverters/IPathConverter.cs
Normal file
@ -0,0 +1,45 @@
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Wabbajack.Paths;
|
||||
|
||||
namespace Wabbajack.DTOs.JsonConverters;
|
||||
|
||||
public class IPathConverter : JsonConverter<IPath>
|
||||
{
|
||||
public override IPath? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType != JsonTokenType.StartObject)
|
||||
throw new JsonException("Invalid format, expected StartObject");
|
||||
|
||||
reader.Read();
|
||||
var type = reader.GetString();
|
||||
reader.Read();
|
||||
var value = reader.GetString();
|
||||
reader.Read();
|
||||
|
||||
|
||||
if (type == "Absolute")
|
||||
return value!.ToAbsolutePath();
|
||||
else
|
||||
return value!.ToRelativePath();
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, IPath value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
switch (value)
|
||||
{
|
||||
case AbsolutePath a:
|
||||
writer.WriteString("Absolute", a.ToString());
|
||||
break;
|
||||
case RelativePath r:
|
||||
writer.WriteString("Relative", r.ToString());
|
||||
break;
|
||||
default:
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
}
|
16
Wabbajack.DTOs/Vfs/IndexedVirtualFile.cs
Normal file
16
Wabbajack.DTOs/Vfs/IndexedVirtualFile.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using System.Collections.Generic;
|
||||
using Wabbajack.DTOs.Texture;
|
||||
using Wabbajack.Hashing.xxHash64;
|
||||
using Wabbajack.Paths;
|
||||
|
||||
namespace Wabbajack.DTOs.Vfs;
|
||||
|
||||
public class IndexedVirtualFile
|
||||
{
|
||||
public IPath Name { get; set; }
|
||||
public Hash Hash { get; set; }
|
||||
|
||||
public ImageState? ImageState { get; set; }
|
||||
public long Size { get; set; }
|
||||
public List<IndexedVirtualFile> Children { get; set; } = new();
|
||||
}
|
@ -44,16 +44,14 @@ public class DownloadDispatcher
|
||||
return await Download(a, dest, job, token);
|
||||
}
|
||||
|
||||
public async Task<Hash> Download(Archive a, AbsolutePath dest, Job<DownloadDispatcher> job, CancellationToken token)
|
||||
public async Task<Archive> MaybeProxy(Archive a, CancellationToken token)
|
||||
{
|
||||
if (!dest.Parent.DirectoryExists())
|
||||
dest.Parent.CreateDirectory();
|
||||
|
||||
var downloader = Downloader(a);
|
||||
if (_useProxyCache && downloader is IProxyable p)
|
||||
if (a.State is not IProxyable p) return a;
|
||||
|
||||
var uri = p.UnParse(a.State);
|
||||
var newUri = await _wjClient.MakeProxyUrl(a, uri);
|
||||
if (newUri != null)
|
||||
{
|
||||
var uri = p.UnParse(a.State);
|
||||
var newUri = _wjClient.MakeProxyUrl(a, uri);
|
||||
a = new Archive
|
||||
{
|
||||
Name = a.Name,
|
||||
@ -64,8 +62,36 @@ public class DownloadDispatcher
|
||||
Url = newUri
|
||||
}
|
||||
};
|
||||
downloader = Downloader(a);
|
||||
_logger.LogInformation("Downloading Proxy ({Hash}) {Uri}", (await uri.ToString().Hash()).ToHex(), uri);
|
||||
}
|
||||
|
||||
return a;
|
||||
}
|
||||
|
||||
public async Task<Hash> Download(Archive a, AbsolutePath dest, Job<DownloadDispatcher> job, CancellationToken token)
|
||||
{
|
||||
if (!dest.Parent.DirectoryExists())
|
||||
dest.Parent.CreateDirectory();
|
||||
|
||||
var downloader = Downloader(a);
|
||||
if (_useProxyCache && downloader is IProxyable p)
|
||||
{
|
||||
var uri = p.UnParse(a.State);
|
||||
var newUri = await _wjClient.MakeProxyUrl(a, uri);
|
||||
if (newUri != null)
|
||||
{
|
||||
a = new Archive
|
||||
{
|
||||
Name = a.Name,
|
||||
Size = a.Size,
|
||||
Hash = a.Hash,
|
||||
State = new DTOs.DownloadStates.Http()
|
||||
{
|
||||
Url = newUri
|
||||
}
|
||||
};
|
||||
downloader = Downloader(a);
|
||||
_logger.LogInformation("Downloading Proxy ({Hash}) {Uri}", (await uri.ToString().Hash()).ToHex(), uri);
|
||||
}
|
||||
}
|
||||
|
||||
var hash = await downloader.Download(a, dest, job, token);
|
||||
@ -160,7 +186,7 @@ public class DownloadDispatcher
|
||||
return DownloadResult.Update;
|
||||
*/
|
||||
}
|
||||
|
||||
|
||||
private async Task<Hash> DownloadFromMirror(Archive archive, AbsolutePath destination, CancellationToken token)
|
||||
{
|
||||
try
|
||||
|
@ -12,7 +12,7 @@ using Wabbajack.RateLimiter;
|
||||
|
||||
namespace Wabbajack.Downloaders.Manual;
|
||||
|
||||
public class ManualDownloader : ADownloader<DTOs.DownloadStates.Manual>
|
||||
public class ManualDownloader : ADownloader<DTOs.DownloadStates.Manual>, IProxyable
|
||||
{
|
||||
private readonly ILogger<ManualDownloader> _logger;
|
||||
private readonly IUserInterventionHandler _interventionHandler;
|
||||
@ -98,4 +98,19 @@ public class ManualDownloader : ADownloader<DTOs.DownloadStates.Manual>
|
||||
|
||||
return new[] {$"manualURL={state.Url}", $"prompt={state.Prompt}"};
|
||||
}
|
||||
|
||||
public IDownloadState? Parse(Uri uri)
|
||||
{
|
||||
return new DTOs.DownloadStates.Manual() {Url = uri};
|
||||
}
|
||||
|
||||
public Uri UnParse(IDownloadState state)
|
||||
{
|
||||
return (state as DTOs.DownloadStates.Manual)!.Url;
|
||||
}
|
||||
|
||||
public Task<T> DownloadStream<T>(Archive archive, Func<Stream, Task<T>> fn, CancellationToken token)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
@ -166,7 +166,7 @@ public class NexusDownloader : ADownloader<Nexus>, IUrlDownloader
|
||||
|
||||
var msg = browserState.ToHttpRequestMessage();
|
||||
|
||||
using var response = await _client.SendAsync(msg, HttpCompletionOption.ResponseHeadersRead, token);
|
||||
using var response = await _client.SendAsync(msg, HttpCompletionOption.ResponseHeadersRead, token);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
throw new HttpRequestException(response.ReasonPhrase, null, statusCode:response.StatusCode);
|
||||
|
||||
|
@ -106,6 +106,7 @@ public abstract class AInstaller<T>
|
||||
_updateStopWatch.Restart();
|
||||
MaxStepProgress = maxStepProgress;
|
||||
_currentStep += 1;
|
||||
_currentStepProgress = 0;
|
||||
_statusText = statusText;
|
||||
_statusCategory = statusCategory;
|
||||
_statusFormatter = formatter ?? (x => x.ToString());
|
||||
@ -313,6 +314,10 @@ public abstract class AInstaller<T>
|
||||
_logger.LogInformation("Downloading {Count} archives", missing.Count.ToString());
|
||||
NextStep(Consts.StepDownloading, "Downloading files", missing.Count);
|
||||
|
||||
missing = await missing
|
||||
.SelectAsync(async m => await _downloadDispatcher.MaybeProxy(m, token))
|
||||
.ToList();
|
||||
|
||||
if (download)
|
||||
{
|
||||
var result = SendDownloadMetrics(missing);
|
||||
@ -479,25 +484,24 @@ public abstract class AInstaller<T>
|
||||
var savePath = (RelativePath) "saves";
|
||||
|
||||
NextStep(Consts.StepPreparing, "Looking for files to delete", 0);
|
||||
await _configuration.Install.EnumerateFiles()
|
||||
.PDoAll(async f =>
|
||||
{
|
||||
var relativeTo = f.RelativeTo(_configuration.Install);
|
||||
if (indexed.ContainsKey(relativeTo) || f.InFolder(_configuration.Downloads))
|
||||
return;
|
||||
foreach (var f in _configuration.Install.EnumerateFiles())
|
||||
{
|
||||
var relativeTo = f.RelativeTo(_configuration.Install);
|
||||
if (indexed.ContainsKey(relativeTo) || f.InFolder(_configuration.Downloads))
|
||||
return;
|
||||
|
||||
if (f.InFolder(profileFolder) && f.Parent.FileName == savePath) return;
|
||||
if (f.InFolder(profileFolder) && f.Parent.FileName == savePath) return;
|
||||
|
||||
if (NoDeleteRegex.IsMatch(f.ToString()))
|
||||
return;
|
||||
if (NoDeleteRegex.IsMatch(f.ToString()))
|
||||
return;
|
||||
|
||||
if (bsaPathsToNotBuild.Contains(f))
|
||||
return;
|
||||
|
||||
_logger.LogInformation("Deleting {RelativePath} it's not part of this ModList", relativeTo);
|
||||
f.Delete();
|
||||
});
|
||||
if (bsaPathsToNotBuild.Contains(f))
|
||||
return;
|
||||
|
||||
_logger.LogInformation("Deleting {RelativePath} it's not part of this ModList", relativeTo);
|
||||
f.Delete();
|
||||
}
|
||||
|
||||
_logger.LogInformation("Cleaning empty folders");
|
||||
var expectedFolders = indexed.Keys
|
||||
.Select(f => f.RelativeTo(_configuration.Install))
|
||||
@ -540,6 +544,7 @@ public abstract class AInstaller<T>
|
||||
// Bit backwards, but we want to return null for
|
||||
// all files we *want* installed. We return the files
|
||||
// to remove from the install list.
|
||||
using var job = await _limiter.Begin($"Hashing File {d.To}", 0, token);
|
||||
var path = _configuration.Install.Combine(d.To);
|
||||
if (!existingfiles.Contains(path)) return null;
|
||||
|
||||
|
@ -28,7 +28,7 @@ public static class IniExtensions
|
||||
/// </summary>
|
||||
/// <param name="file"></param>
|
||||
/// <returns></returns>
|
||||
public static IniData LoadIniFile(this AbsolutePath file)
|
||||
public static IniData LoadIniFile(this AbsolutePath file)
|
||||
{
|
||||
return new FileIniDataParser(IniParser()).ReadFile(file.ToString());
|
||||
}
|
||||
|
@ -1,5 +1,9 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@ -28,12 +32,109 @@ public class SingleThreadedDownloader : IHttpDownloader
|
||||
using var response = await _client.SendAsync(message, HttpCompletionOption.ResponseHeadersRead, token);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
throw new HttpException(response);
|
||||
|
||||
|
||||
if (job.Size == 0)
|
||||
job.Size = response.Content.Headers.ContentLength ?? 0;
|
||||
|
||||
/* Need to make this mulitthreaded to be much use
|
||||
if ((response.Content.Headers.ContentLength ?? 0) != 0 &&
|
||||
response.Headers.AcceptRanges.FirstOrDefault() == "bytes")
|
||||
{
|
||||
return await ResettingDownloader(response, message, outputPath, job, token);
|
||||
}
|
||||
*/
|
||||
|
||||
await using var stream = await response.Content.ReadAsStreamAsync(token);
|
||||
await using var outputStream = outputPath.Open(FileMode.Create, FileAccess.Write);
|
||||
return await stream.HashingCopy(outputStream, token, job);
|
||||
}
|
||||
|
||||
private const int CHUNK_SIZE = 1024 * 1024 * 8;
|
||||
|
||||
private async Task<Hash> ResettingDownloader(HttpResponseMessage response, HttpRequestMessage message, AbsolutePath outputPath, IJob job, CancellationToken token)
|
||||
{
|
||||
|
||||
using var rented = MemoryPool<byte>.Shared.Rent(CHUNK_SIZE);
|
||||
var buffer = rented.Memory;
|
||||
|
||||
var hasher = new xxHashAlgorithm(0);
|
||||
|
||||
var running = true;
|
||||
ulong finalHash = 0;
|
||||
|
||||
var inputStream = await response.Content.ReadAsStreamAsync(token);
|
||||
await using var outputStream = outputPath.Open(FileMode.Create, FileAccess.Write, FileShare.None);
|
||||
long writePosition = 0;
|
||||
|
||||
while (running && !token.IsCancellationRequested)
|
||||
{
|
||||
var totalRead = 0;
|
||||
|
||||
while (totalRead != buffer.Length)
|
||||
{
|
||||
var read = await inputStream.ReadAsync(buffer.Slice(totalRead, buffer.Length - totalRead),
|
||||
token);
|
||||
|
||||
|
||||
if (read == 0)
|
||||
{
|
||||
running = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (job != null)
|
||||
await job.Report(read, token);
|
||||
|
||||
totalRead += read;
|
||||
}
|
||||
|
||||
var pendingWrite = outputStream.WriteAsync(buffer[..totalRead], token);
|
||||
if (running)
|
||||
{
|
||||
hasher.TransformByteGroupsInternal(buffer.Span);
|
||||
await pendingWrite;
|
||||
}
|
||||
else
|
||||
{
|
||||
var preSize = (totalRead >> 5) << 5;
|
||||
if (preSize > 0)
|
||||
{
|
||||
hasher.TransformByteGroupsInternal(buffer[..preSize].Span);
|
||||
finalHash = hasher.FinalizeHashValueInternal(buffer[preSize..totalRead].Span);
|
||||
await pendingWrite;
|
||||
break;
|
||||
}
|
||||
|
||||
finalHash = hasher.FinalizeHashValueInternal(buffer[..totalRead].Span);
|
||||
await pendingWrite;
|
||||
break;
|
||||
}
|
||||
|
||||
{
|
||||
writePosition += totalRead;
|
||||
await job.Report(totalRead, token);
|
||||
message = CloneMessage(message);
|
||||
message.Headers.Range = new RangeHeaderValue(writePosition, writePosition + CHUNK_SIZE);
|
||||
await inputStream.DisposeAsync();
|
||||
response.Dispose();
|
||||
response = await _client.SendAsync(message, HttpCompletionOption.ResponseHeadersRead, token);
|
||||
HttpException.ThrowOnFailure(response);
|
||||
inputStream = await response.Content.ReadAsStreamAsync(token);
|
||||
}
|
||||
}
|
||||
|
||||
await outputStream.FlushAsync(token);
|
||||
|
||||
return new Hash(finalHash);
|
||||
}
|
||||
|
||||
private HttpRequestMessage CloneMessage(HttpRequestMessage message)
|
||||
{
|
||||
var newMsg = new HttpRequestMessage(message.Method, message.RequestUri);
|
||||
foreach (var header in message.Headers)
|
||||
{
|
||||
newMsg.Headers.Add(header.Key, header.Value);
|
||||
}
|
||||
return newMsg;
|
||||
}
|
||||
}
|
39
Wabbajack.Networking.WabbajackClientApi/CesiVFSCache.cs
Normal file
39
Wabbajack.Networking.WabbajackClientApi/CesiVFSCache.cs
Normal file
@ -0,0 +1,39 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Wabbajack.DTOs.Vfs;
|
||||
using Wabbajack.Hashing.xxHash64;
|
||||
using Wabbajack.Networking.Http;
|
||||
using Wabbajack.VFS.Interfaces;
|
||||
|
||||
namespace Wabbajack.Networking.WabbajackClientApi;
|
||||
|
||||
public class CesiVFSCache : IVfsCache
|
||||
{
|
||||
private readonly Client _client;
|
||||
private readonly ILogger<CesiVFSCache> _logger;
|
||||
|
||||
public CesiVFSCache(ILogger<CesiVFSCache> logger, Client client)
|
||||
{
|
||||
_logger = logger;
|
||||
_client = client;
|
||||
}
|
||||
|
||||
public async Task<IndexedVirtualFile?> Get(Hash hash, CancellationToken token)
|
||||
{
|
||||
_logger.LogInformation("Requesting CESI Information for: {Hash}", hash.ToHex());
|
||||
try
|
||||
{
|
||||
return await _client.GetCesiVfsEntry(hash, token);
|
||||
}
|
||||
catch (HttpException exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Put(IndexedVirtualFile file, CancellationToken token)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
@ -8,22 +8,24 @@ using System.Net.Http.Json;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Wabbajack.Common;
|
||||
using Wabbajack.DTOs;
|
||||
using Wabbajack.DTOs.CDN;
|
||||
using Wabbajack.DTOs.Configs;
|
||||
using Wabbajack.DTOs.DownloadStates;
|
||||
using Wabbajack.DTOs.JsonConverters;
|
||||
using Wabbajack.DTOs.Logins;
|
||||
using Wabbajack.DTOs.ModListValidation;
|
||||
using Wabbajack.DTOs.Validation;
|
||||
using Wabbajack.DTOs.Vfs;
|
||||
using Wabbajack.Hashing.xxHash64;
|
||||
using Wabbajack.Networking.Http;
|
||||
using Wabbajack.Networking.Http.Interfaces;
|
||||
using Wabbajack.Paths;
|
||||
using Wabbajack.Paths.IO;
|
||||
using Wabbajack.RateLimiter;
|
||||
using Wabbajack.VFS;
|
||||
using YamlDotNet.Serialization;
|
||||
using YamlDotNet.Serialization.NamingConventions;
|
||||
|
||||
@ -36,7 +38,7 @@ public class Client
|
||||
private readonly HttpClient _client;
|
||||
private readonly Configuration _configuration;
|
||||
private readonly DTOSerializer _dtos;
|
||||
private readonly IResource<FileHashCache> _hashLimiter;
|
||||
private readonly IResource<Client> _hashLimiter;
|
||||
private readonly IResource<HttpClient> _limiter;
|
||||
private readonly ILogger<Client> _logger;
|
||||
private readonly ParallelOptions _parallelOptions;
|
||||
@ -46,7 +48,7 @@ public class Client
|
||||
|
||||
public Client(ILogger<Client> logger, HttpClient client, ITokenProvider<WabbajackApiState> token,
|
||||
DTOSerializer dtos,
|
||||
IResource<HttpClient> limiter, IResource<FileHashCache> hashLimiter, Configuration configuration)
|
||||
IResource<HttpClient> limiter, IResource<Client> hashLimiter, Configuration configuration)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_token = token;
|
||||
@ -362,9 +364,35 @@ public class Client
|
||||
var url = $"https://raw.githubusercontent.com/wabbajack-tools/indexed-game-files/master/{game}/{version}_steam_manifests.json";
|
||||
return await _client.GetFromJsonAsync<SteamManifest[]>(url, _dtos.Options) ?? Array.Empty<SteamManifest>();
|
||||
}
|
||||
|
||||
public Uri MakeProxyUrl(Archive archive, Uri uri)
|
||||
|
||||
public async Task<bool> ProxyHas(Uri uri)
|
||||
{
|
||||
return new Uri($"{_configuration.BuildServerUrl}proxy?name={archive.Name}&hash={archive.Hash.ToHex()}&uri={uri}");
|
||||
var newUri = new Uri($"{_configuration.BuildServerUrl}proxy?uri={HttpUtility.UrlEncode(uri.ToString())}");
|
||||
var msg = new HttpRequestMessage(HttpMethod.Head, newUri);
|
||||
try
|
||||
{
|
||||
var result = await _client.SendAsync(msg);
|
||||
return result.IsSuccessStatusCode;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Uri?> MakeProxyUrl(Archive archive, Uri uri)
|
||||
{
|
||||
if (archive.State is Manual && !await ProxyHas(uri))
|
||||
return null;
|
||||
|
||||
return new Uri($"{_configuration.BuildServerUrl}proxy?name={archive.Name}&hash={archive.Hash.ToHex()}&uri={HttpUtility.UrlEncode(uri.ToString())}");
|
||||
}
|
||||
|
||||
public async Task<IndexedVirtualFile?> GetCesiVfsEntry(Hash hash, CancellationToken token)
|
||||
{
|
||||
var msg = await MakeMessage(HttpMethod.Get, new Uri($"{_configuration.BuildServerUrl}cesi/vfs/{hash.ToHex()}"));
|
||||
using var response = await _client.SendAsync(msg, token);
|
||||
HttpException.ThrowOnFailure(response);
|
||||
return await _dtos.DeserializeAsync<IndexedVirtualFile>(await response.Content.ReadAsStreamAsync(token), token);
|
||||
}
|
||||
}
|
@ -16,7 +16,7 @@
|
||||
<ProjectReference Include="..\Wabbajack.Common\Wabbajack.Common.csproj" />
|
||||
<ProjectReference Include="..\Wabbajack.DTOs\Wabbajack.DTOs.csproj" />
|
||||
<ProjectReference Include="..\Wabbajack.Paths.IO\Wabbajack.Paths.IO.csproj" />
|
||||
<ProjectReference Include="..\Wabbajack.VFS\Wabbajack.VFS.csproj" />
|
||||
<ProjectReference Include="..\Wabbajack.VFS.Interfaces\Wabbajack.VFS.Interfaces.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -12,7 +12,7 @@ public static class KnownFolders
|
||||
get
|
||||
{
|
||||
var result = Process.GetCurrentProcess().MainModule!.FileName!.ToAbsolutePath().Parent;
|
||||
if (result.FileName == "dotnet".ToRelativePath())
|
||||
if (result.FileName == "dotnet".ToRelativePath() || Assembly.GetEntryAssembly() != null)
|
||||
{
|
||||
return Assembly.GetExecutingAssembly().Location.ToAbsolutePath().Parent;
|
||||
}
|
||||
|
@ -33,4 +33,15 @@ public class AppSettings
|
||||
public string MetricsFolder { get; set; } = "";
|
||||
public string TarLogPath { get; set; }
|
||||
public string GitHubKey { get; set; } = "";
|
||||
|
||||
public CouchDBSetting CesiDB { get; set; }
|
||||
public CouchDBSetting MetricsDB { get; set; }
|
||||
}
|
||||
|
||||
public class CouchDBSetting
|
||||
{
|
||||
public Uri Endpoint { get; set; }
|
||||
public string Database { get; set; }
|
||||
public string Username { get; set; }
|
||||
public string Password { get; set; }
|
||||
}
|
106
Wabbajack.Server/Controllers/Cesi.cs
Normal file
106
Wabbajack.Server/Controllers/Cesi.cs
Normal file
@ -0,0 +1,106 @@
|
||||
using cesi.DTOs;
|
||||
using CouchDB.Driver;
|
||||
using CouchDB.Driver.Views;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Wabbajack.Common;
|
||||
using Wabbajack.DTOs.JsonConverters;
|
||||
using Wabbajack.DTOs.Texture;
|
||||
using Wabbajack.DTOs.Vfs;
|
||||
using Wabbajack.Hashing.xxHash64;
|
||||
using Wabbajack.Paths;
|
||||
using Wabbajack.VFS;
|
||||
|
||||
namespace Wabbajack.Server.Controllers;
|
||||
|
||||
[Route("/cesi")]
|
||||
public class Cesi : ControllerBase
|
||||
{
|
||||
private readonly ILogger<Cesi> _logger;
|
||||
private readonly ICouchDatabase<Analyzed> _db;
|
||||
private readonly DTOSerializer _dtos;
|
||||
|
||||
public Cesi(ILogger<Cesi> logger, ICouchDatabase<Analyzed> db, DTOSerializer serializer)
|
||||
{
|
||||
_logger = logger;
|
||||
_db = db;
|
||||
_dtos = serializer;
|
||||
}
|
||||
|
||||
[HttpGet("entry/{hash}")]
|
||||
public async Task<IActionResult> Entry(string hash)
|
||||
{
|
||||
return Ok(await _db.FindAsync(hash));
|
||||
}
|
||||
|
||||
[HttpGet("vfs/{hash}")]
|
||||
public async Task<IActionResult> Vfs(string hash)
|
||||
{
|
||||
var entry = await _db.FindAsync(ReverseHash(hash));
|
||||
if (entry == null) return NotFound(new {Message = "Entry not found", Hash = hash, ReverseHash = ReverseHash(hash)});
|
||||
|
||||
|
||||
var indexed = new IndexedVirtualFile
|
||||
{
|
||||
Hash = Hash.FromHex(ReverseHash(entry.xxHash64)),
|
||||
Size = entry.Size,
|
||||
ImageState = GetImageState(entry),
|
||||
Children = await GetChildrenState(entry),
|
||||
};
|
||||
|
||||
|
||||
return Ok(_dtos.Serialize(indexed, true));
|
||||
}
|
||||
|
||||
private async Task<List<IndexedVirtualFile>> GetChildrenState(Analyzed entry)
|
||||
{
|
||||
if (entry.Archive == null) return new List<IndexedVirtualFile>();
|
||||
|
||||
var children = await _db.GetViewAsync<string, Analyzed>("Indexes", "ArchiveContents", new CouchViewOptions<string>
|
||||
{
|
||||
IncludeDocs = true,
|
||||
Key = entry.xxHash64
|
||||
});
|
||||
|
||||
var indexed = children.ToLookup(d => d.Document.xxHash64, v => v.Document);
|
||||
|
||||
return await entry.Archive.Entries.SelectAsync(async e =>
|
||||
{
|
||||
var found = indexed[e.Value].First();
|
||||
return new IndexedVirtualFile
|
||||
{
|
||||
Name = e.Key.ToRelativePath(),
|
||||
Size = found.Size,
|
||||
Hash = Hash.FromHex(ReverseHash(found.xxHash64)),
|
||||
ImageState = GetImageState(found),
|
||||
Children = await GetChildrenState(found),
|
||||
};
|
||||
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
private ImageState? GetImageState(Analyzed entry)
|
||||
{
|
||||
if (entry.DDS == null) return null;
|
||||
return new ImageState
|
||||
{
|
||||
Width = entry.DDS.Width,
|
||||
Height = entry.DDS.Height,
|
||||
Format = Enum.Parse<DXGI_FORMAT>(entry.DDS.Format),
|
||||
PerceptualHash = new PHash(entry.DDS.PHash.FromHex())
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private Hash ReverseHash(Hash hash)
|
||||
{
|
||||
return Hash.FromHex(hash.ToArray().Reverse().ToArray().ToHex());
|
||||
}
|
||||
private string ReverseHash(string hash)
|
||||
{
|
||||
return hash.FromHex().Reverse().ToArray().ToHex();
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using Chronic.Core;
|
||||
using CouchDB.Driver;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Nettle;
|
||||
@ -35,13 +36,15 @@ public class MetricsController : ControllerBase
|
||||
private readonly AppSettings _settings;
|
||||
private ILogger<MetricsController> _logger;
|
||||
private readonly Metrics _metricsStore;
|
||||
private readonly ICouchDatabase<Metric> _db;
|
||||
|
||||
public MetricsController(ILogger<MetricsController> logger, Metrics metricsStore,
|
||||
AppSettings settings)
|
||||
AppSettings settings, ICouchDatabase<Metric> db)
|
||||
{
|
||||
_logger = logger;
|
||||
_settings = settings;
|
||||
_metricsStore = metricsStore;
|
||||
_db = db;
|
||||
}
|
||||
|
||||
|
||||
@ -88,7 +91,7 @@ public class MetricsController : ControllerBase
|
||||
private static byte[] LBRACKET = {(byte)'['};
|
||||
private static byte[] RBRACKET = {(byte)']'};
|
||||
private static byte[] COMMA = {(byte) ','};
|
||||
|
||||
|
||||
[HttpGet]
|
||||
[Route("dump")]
|
||||
public async Task GetMetrics([FromQuery] string action, [FromQuery] string from, [FromQuery] string? to, [FromQuery] string? subject)
|
||||
|
@ -1,9 +1,10 @@
|
||||
using System;
|
||||
using CouchDB.Driver.Types;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace Wabbajack.Server.DTOs;
|
||||
|
||||
public class Metric
|
||||
public class Metric : CouchDocument
|
||||
{
|
||||
public DateTime Timestamp { get; set; }
|
||||
public string Action { get; set; }
|
||||
|
@ -34,8 +34,10 @@ public class NexusCacheManager
|
||||
_nexusAPI = nexusApi;
|
||||
_discord = discord;
|
||||
|
||||
/* TODO - uncomment me!
|
||||
_timer = new Timer(_ => UpdateNexusCacheAPI().FireAndForget(), null, TimeSpan.FromSeconds(2),
|
||||
TimeSpan.FromHours(4));
|
||||
*/
|
||||
}
|
||||
|
||||
|
||||
|
@ -2,7 +2,12 @@
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using cesi.DTOs;
|
||||
using CouchDB.Driver;
|
||||
using CouchDB.Driver.Options;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
@ -33,7 +38,9 @@ using Wabbajack.Server.Services;
|
||||
using Wabbajack.Services.OSIntegrated.TokenProviders;
|
||||
using Wabbajack.Networking.WabbajackClientApi;
|
||||
using Wabbajack.Paths.IO;
|
||||
using Wabbajack.Server.DTOs;
|
||||
using Wabbajack.VFS;
|
||||
using YamlDotNet.Serialization.NamingConventions;
|
||||
using Client = Wabbajack.Networking.GitHub.Client;
|
||||
|
||||
namespace Wabbajack.Server;
|
||||
@ -135,10 +142,40 @@ public class Startup
|
||||
options.Providers.Add<GzipCompressionProvider>();
|
||||
options.MimeTypes = new[] {"application/json"};
|
||||
});
|
||||
|
||||
// CouchDB
|
||||
services.AddSingleton(s =>
|
||||
{
|
||||
var settings = s.GetRequiredService<AppSettings>();
|
||||
var client = new CouchClient(settings.CesiDB.Endpoint, b =>
|
||||
{
|
||||
b.UseBasicAuthentication("cesi", "password");
|
||||
b.SetPropertyCase(PropertyCaseType.None);
|
||||
b.SetJsonNullValueHandling(NullValueHandling.Ignore);
|
||||
});
|
||||
return client.GetDatabase<Analyzed>("cesi");
|
||||
});
|
||||
|
||||
services.AddSingleton(s =>
|
||||
{
|
||||
var settings = s.GetRequiredService<AppSettings>();
|
||||
var client = new CouchClient(settings.CesiDB.Endpoint, b =>
|
||||
{
|
||||
b.UseBasicAuthentication("wabbajack", "password");
|
||||
b.SetPropertyCase(PropertyCaseType.None);
|
||||
b.SetJsonNullValueHandling(NullValueHandling.Ignore);
|
||||
});
|
||||
return client.GetDatabase<Metric>("cesi");
|
||||
});
|
||||
|
||||
services.AddMvc();
|
||||
services.AddControllers()
|
||||
.AddNewtonsoftJson(o => { o.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; });
|
||||
services
|
||||
.AddControllers()
|
||||
.AddJsonOptions(j =>
|
||||
{
|
||||
j.JsonSerializerOptions.PropertyNamingPolicy = null;
|
||||
j.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault;
|
||||
});
|
||||
|
||||
NettleEngine.GetCompiler().RegisterWJFunctions();
|
||||
}
|
||||
|
@ -8,6 +8,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="cesi.DTOs" Version="1.0.0" />
|
||||
<PackageReference Include="Chronic.Core" Version="0.4.0" />
|
||||
<PackageReference Include="Dapper" Version="2.0.123" />
|
||||
<PackageReference Include="Discord.Net.WebSocket" Version="3.6.1" />
|
||||
|
@ -15,7 +15,19 @@
|
||||
"MirrorFilesFolder": "c:\\tmp\\mirrors",
|
||||
"NexusCacheFolder": "c:\\tmp\\nexus-cache",
|
||||
"ProxyFolder": "c:\\tmp\\proxy",
|
||||
"GitHubKey": ""
|
||||
"GitHubKey": "",
|
||||
"CesiDB": {
|
||||
"Endpoint": "http://localhost:15984",
|
||||
"Database": "cesi",
|
||||
"Username": "cesi",
|
||||
"Password": "password"
|
||||
},
|
||||
"MetricsDB": {
|
||||
"Endpoint": "http://localhost:15984",
|
||||
"Database": "metrics",
|
||||
"Username": "wabbajack",
|
||||
"Password": "password"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ using Wabbajack.RateLimiter;
|
||||
using Wabbajack.Services.OSIntegrated.Services;
|
||||
using Wabbajack.Services.OSIntegrated.TokenProviders;
|
||||
using Wabbajack.VFS;
|
||||
using Wabbajack.VFS.Interfaces;
|
||||
using Client = Wabbajack.Networking.WabbajackClientApi.Client;
|
||||
|
||||
namespace Wabbajack.Services.OSIntegrated;
|
||||
@ -55,9 +56,9 @@ public static class ServiceExtensions
|
||||
: new FileHashCache(KnownFolders.AppDataLocal.Combine("Wabbajack", "GlobalHashCache.sqlite"),
|
||||
s.GetService<IResource<FileHashCache>>()!));
|
||||
|
||||
service.AddSingleton(s => options.UseLocalCache
|
||||
? new VFSCache(s.GetService<TemporaryFileManager>()!.CreateFile().Path)
|
||||
: new VFSCache(KnownFolders.EntryPoint.Combine("GlobalVFSCache3.sqlite")));
|
||||
service.AddAllSingleton<IVfsCache, VFSDiskCache>(s => options.UseLocalCache
|
||||
? new VFSDiskCache(s.GetService<TemporaryFileManager>()!.CreateFile().Path)
|
||||
: new VFSDiskCache(KnownFolders.EntryPoint.Combine("GlobalVFSCache3.sqlite")));
|
||||
|
||||
service.AddSingleton<IBinaryPatchCache>(s => options.UseLocalCache
|
||||
? new BinaryPatchCache(s.GetService<TemporaryFileManager>()!.CreateFile().Path)
|
||||
@ -97,6 +98,8 @@ public static class ServiceExtensions
|
||||
service.AddAllSingleton<IResource, IResource<Context>>(s => new Resource<Context>("VFS", GetSettings(s, "VFS")));
|
||||
service.AddAllSingleton<IResource, IResource<FileHashCache>>(s =>
|
||||
new Resource<FileHashCache>("File Hashing", GetSettings(s, "File Hashing")));
|
||||
service.AddAllSingleton<IResource, IResource<Client>>(s =>
|
||||
new Resource<Client>("Wabbajack Client", GetSettings(s, "Wabbajack Client")));
|
||||
service.AddAllSingleton<IResource, IResource<FileExtractor.FileExtractor>>(s =>
|
||||
new Resource<FileExtractor.FileExtractor>("File Extractor", GetSettings(s, "File Extractor")));
|
||||
|
||||
|
10
Wabbajack.VFS.Interfaces/IVfsCache.cs
Normal file
10
Wabbajack.VFS.Interfaces/IVfsCache.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using Wabbajack.DTOs.Vfs;
|
||||
using Wabbajack.Hashing.xxHash64;
|
||||
|
||||
namespace Wabbajack.VFS.Interfaces;
|
||||
|
||||
public interface IVfsCache
|
||||
{
|
||||
public Task<IndexedVirtualFile?> Get(Hash hash, CancellationToken token);
|
||||
public Task Put(IndexedVirtualFile file, CancellationToken token);
|
||||
}
|
15
Wabbajack.VFS.Interfaces/Wabbajack.VFS.Interfaces.csproj
Normal file
15
Wabbajack.VFS.Interfaces/Wabbajack.VFS.Interfaces.csproj
Normal file
@ -0,0 +1,15 @@
|
||||
<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.Hashing.xxHash64\Wabbajack.Hashing.xxHash64.csproj" />
|
||||
<ProjectReference Include="..\Wabbajack.Paths\Wabbajack.Paths.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
@ -4,6 +4,7 @@ using Microsoft.Extensions.Logging;
|
||||
using Wabbajack.DTOs;
|
||||
using Wabbajack.Paths.IO;
|
||||
using Wabbajack.RateLimiter;
|
||||
using Wabbajack.VFS.Interfaces;
|
||||
using Xunit.DependencyInjection;
|
||||
using Xunit.DependencyInjection.Logging;
|
||||
|
||||
@ -32,7 +33,7 @@ public class Startup
|
||||
service.AddSingleton(new ParallelOptions {MaxDegreeOfParallelism = 2});
|
||||
service.AddSingleton(new FileHashCache(KnownFolders.EntryPoint.Combine("hashcache.sqlite"),
|
||||
new Resource<FileHashCache>("File Hashing", 10)));
|
||||
service.AddSingleton(new VFSCache(KnownFolders.EntryPoint.Combine("vfscache.sqlite")));
|
||||
service.AddAllSingleton<IVfsCache, VFSDiskCache>(x => new VFSDiskCache(KnownFolders.EntryPoint.Combine("vfscache.sqlite")));
|
||||
service.AddTransient<Context>();
|
||||
service.AddSingleton<FileExtractor.FileExtractor>();
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ using Wabbajack.Hashing.xxHash64;
|
||||
using Wabbajack.Paths;
|
||||
using Wabbajack.Paths.IO;
|
||||
using Wabbajack.RateLimiter;
|
||||
using Wabbajack.VFS.Interfaces;
|
||||
|
||||
namespace Wabbajack.VFS;
|
||||
|
||||
@ -27,10 +28,10 @@ public class Context
|
||||
public readonly IResource<Context> Limiter;
|
||||
public readonly IResource<FileHashCache> HashLimiter;
|
||||
public readonly ILogger<Context> Logger;
|
||||
public readonly VFSCache VfsCache;
|
||||
public readonly IVfsCache VfsCache;
|
||||
|
||||
public Context(ILogger<Context> logger, ParallelOptions parallelOptions, TemporaryFileManager manager,
|
||||
VFSCache vfsCache,
|
||||
IVfsCache vfsCache,
|
||||
FileHashCache hashCache, IResource<Context> limiter, IResource<FileHashCache> hashLimiter, FileExtractor.FileExtractor extractor)
|
||||
{
|
||||
Limiter = limiter;
|
||||
|
37
Wabbajack.VFS/FallthroughVFSCache.cs
Normal file
37
Wabbajack.VFS/FallthroughVFSCache.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Wabbajack.DTOs.Vfs;
|
||||
using Wabbajack.Hashing.xxHash64;
|
||||
using Wabbajack.VFS.Interfaces;
|
||||
|
||||
namespace Wabbajack.VFS;
|
||||
|
||||
public class FallthroughVFSCache : IVfsCache
|
||||
{
|
||||
private readonly IVfsCache[] _caches;
|
||||
|
||||
public FallthroughVFSCache(IVfsCache[] caches)
|
||||
{
|
||||
_caches = caches;
|
||||
}
|
||||
|
||||
public async Task<IndexedVirtualFile?> Get(Hash hash, CancellationToken token)
|
||||
{
|
||||
foreach (var cache in _caches)
|
||||
{
|
||||
var result = await cache.Get(hash, token);
|
||||
if (result == null) continue;
|
||||
return result;
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
public async Task Put(IndexedVirtualFile file, CancellationToken token)
|
||||
{
|
||||
foreach (var cache in _caches)
|
||||
{
|
||||
await cache.Put(file, token);
|
||||
}
|
||||
}
|
||||
}
|
@ -3,42 +3,36 @@ using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Text;
|
||||
using Wabbajack.DTOs.Texture;
|
||||
using Wabbajack.DTOs.Vfs;
|
||||
using Wabbajack.Hashing.xxHash64;
|
||||
using Wabbajack.Paths;
|
||||
|
||||
namespace Wabbajack.VFS;
|
||||
|
||||
public class IndexedVirtualFile
|
||||
public static class IndexedVirtualFileExtensions
|
||||
{
|
||||
public IPath Name { get; set; }
|
||||
public Hash Hash { get; set; }
|
||||
|
||||
public ImageState? ImageState { get; set; }
|
||||
public long Size { get; set; }
|
||||
public List<IndexedVirtualFile> Children { get; set; } = new();
|
||||
|
||||
private void Write(BinaryWriter bw)
|
||||
public static void Write(this IndexedVirtualFile ivf, BinaryWriter bw)
|
||||
{
|
||||
bw.Write(Name.ToString()!);
|
||||
bw.Write((ulong) Hash);
|
||||
bw.Write(ivf.Name.ToString()!);
|
||||
bw.Write((ulong) ivf.Hash);
|
||||
|
||||
if (ImageState == null)
|
||||
if (ivf.ImageState == null)
|
||||
{
|
||||
bw.Write(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
bw.Write(true);
|
||||
WriteImageState(bw, ImageState);
|
||||
WriteImageState(bw, ivf.ImageState);
|
||||
}
|
||||
|
||||
bw.Write(Size);
|
||||
bw.Write(Children.Count);
|
||||
foreach (var file in Children)
|
||||
bw.Write(ivf.Size);
|
||||
bw.Write(ivf.Children.Count);
|
||||
foreach (var file in ivf.Children)
|
||||
file.Write(bw);
|
||||
}
|
||||
|
||||
private void WriteImageState(BinaryWriter bw, ImageState state)
|
||||
private static void WriteImageState(BinaryWriter bw, ImageState state)
|
||||
{
|
||||
bw.Write((ushort) state.Width);
|
||||
bw.Write((ushort) state.Height);
|
||||
@ -46,7 +40,7 @@ public class IndexedVirtualFile
|
||||
state.PerceptualHash.Write(bw);
|
||||
}
|
||||
|
||||
public static ImageState ReadImageState(BinaryReader br)
|
||||
static ImageState ReadImageState(BinaryReader br)
|
||||
{
|
||||
return new ImageState
|
||||
{
|
||||
@ -58,14 +52,14 @@ public class IndexedVirtualFile
|
||||
}
|
||||
|
||||
|
||||
public void Write(Stream s)
|
||||
public static void Write(this IndexedVirtualFile ivf, Stream s)
|
||||
{
|
||||
using var cs = new GZipStream(s, CompressionLevel.Optimal, true);
|
||||
using var bw = new BinaryWriter(cs, Encoding.UTF8, true);
|
||||
Write(bw);
|
||||
ivf.Write(bw);
|
||||
}
|
||||
|
||||
private static IndexedVirtualFile Read(BinaryReader br)
|
||||
public static IndexedVirtualFile Read(BinaryReader br)
|
||||
{
|
||||
var ivf = new IndexedVirtualFile
|
||||
{
|
@ -4,22 +4,25 @@ using System.Data;
|
||||
using System.Data.SQLite;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Wabbajack.Common;
|
||||
using Wabbajack.DTOs.Streams;
|
||||
using Wabbajack.DTOs.Vfs;
|
||||
using Wabbajack.Hashing.xxHash64;
|
||||
using Wabbajack.Paths;
|
||||
using Wabbajack.Paths.IO;
|
||||
using Wabbajack.VFS.Interfaces;
|
||||
|
||||
namespace Wabbajack.VFS;
|
||||
|
||||
public class VFSCache
|
||||
public class VFSDiskCache : IVfsCache
|
||||
{
|
||||
private readonly SQLiteConnection _conn;
|
||||
private readonly string _connectionString;
|
||||
private readonly AbsolutePath _path;
|
||||
|
||||
public VFSCache(AbsolutePath path)
|
||||
public VFSDiskCache(AbsolutePath path)
|
||||
{
|
||||
_path = path;
|
||||
|
||||
@ -38,63 +41,34 @@ public class VFSCache
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
public bool TryGetFromCache(Context context, VirtualFile parent, IPath path, IStreamFactory extractedFile,
|
||||
Hash hash, out VirtualFile found)
|
||||
public async Task<IndexedVirtualFile?> Get(Hash hash, CancellationToken token)
|
||||
{
|
||||
if (hash == default)
|
||||
throw new ArgumentException("Cannot cache default hashes");
|
||||
|
||||
using var cmd = new SQLiteCommand(_conn);
|
||||
|
||||
await using var cmd = new SQLiteCommand(_conn);
|
||||
cmd.CommandText = @"SELECT Contents FROM VFSCache WHERE Hash = @hash";
|
||||
cmd.Parameters.AddWithValue("@hash", (long) hash);
|
||||
|
||||
using var rdr = cmd.ExecuteReader();
|
||||
await using var rdr = cmd.ExecuteReader();
|
||||
while (rdr.Read())
|
||||
{
|
||||
var data = IndexedVirtualFile.Read(rdr.GetStream(0));
|
||||
found = ConvertFromIndexedFile(context, data, path, parent, extractedFile);
|
||||
found.Name = path;
|
||||
found.Hash = hash;
|
||||
return true;
|
||||
var data = IndexedVirtualFileExtensions.Read(rdr.GetStream(0));
|
||||
return data;
|
||||
}
|
||||
|
||||
found = default;
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
private static VirtualFile ConvertFromIndexedFile(Context context, IndexedVirtualFile file, IPath path,
|
||||
VirtualFile vparent, IStreamFactory extractedFile)
|
||||
{
|
||||
var vself = new VirtualFile
|
||||
{
|
||||
Context = context,
|
||||
Name = path,
|
||||
Parent = vparent,
|
||||
Size = file.Size,
|
||||
LastModified = extractedFile.LastModifiedUtc.AsUnixTime(),
|
||||
LastAnalyzed = DateTime.Now.AsUnixTime(),
|
||||
Hash = file.Hash,
|
||||
ImageState = file.ImageState
|
||||
};
|
||||
|
||||
vself.FillFullPath();
|
||||
|
||||
vself.Children = file.Children.Select(f => ConvertFromIndexedFile(context, f, f.Name, vself, extractedFile))
|
||||
.ToImmutableList();
|
||||
|
||||
return vself;
|
||||
}
|
||||
|
||||
public async Task WriteToCache(VirtualFile self)
|
||||
|
||||
public async Task Put(IndexedVirtualFile ivf, CancellationToken token)
|
||||
{
|
||||
await using var ms = new MemoryStream();
|
||||
var ivf = self.ToIndexedVirtualFile();
|
||||
// Top level path gets renamed when read, we don't want the absolute path
|
||||
// here else the reader will blow up when it tries to convert the value
|
||||
ivf.Name = (RelativePath) "not/applicable";
|
||||
ivf.Write(ms);
|
||||
ms.Position = 0;
|
||||
await InsertIntoVFSCache(self.Hash, ms);
|
||||
await InsertIntoVFSCache(ivf.Hash, ms);
|
||||
}
|
||||
|
||||
private async Task InsertIntoVFSCache(Hash hash, MemoryStream data)
|
||||
|
@ -10,6 +10,7 @@ using Wabbajack.Common;
|
||||
using Wabbajack.Common.FileSignatures;
|
||||
using Wabbajack.DTOs.Streams;
|
||||
using Wabbajack.DTOs.Texture;
|
||||
using Wabbajack.DTOs.Vfs;
|
||||
using Wabbajack.Hashing.PHash;
|
||||
using Wabbajack.Hashing.xxHash64;
|
||||
using Wabbajack.Paths;
|
||||
@ -176,9 +177,13 @@ public class VirtualFile
|
||||
hash = await hstream.HashingCopy(Stream.Null, token, job);
|
||||
}
|
||||
|
||||
if (context.VfsCache.TryGetFromCache(context, parent, relPath, extractedFile, hash, out var vself))
|
||||
return vself;
|
||||
|
||||
var found = await context.VfsCache.Get(hash, token);
|
||||
if (found != null)
|
||||
{
|
||||
var file = ConvertFromIndexedFile(context, found!, relPath, parent!, extractedFile);
|
||||
file.Name = relPath;
|
||||
return file;
|
||||
}
|
||||
|
||||
await using var stream = await extractedFile.GetStream();
|
||||
var sig = await FileExtractor.FileExtractor.ArchiveSigs.MatchesAsync(stream);
|
||||
@ -219,7 +224,7 @@ public class VirtualFile
|
||||
if (!sig.HasValue ||
|
||||
!FileExtractor.FileExtractor.ExtractableExtensions.Contains(relPath.FileName.Extension))
|
||||
{
|
||||
await context.VfsCache.WriteToCache(self);
|
||||
await context.VfsCache.Put(self.ToIndexedVirtualFile(), token);
|
||||
return self;
|
||||
}
|
||||
|
||||
@ -242,7 +247,7 @@ public class VirtualFile
|
||||
throw;
|
||||
}
|
||||
|
||||
await context.VfsCache.WriteToCache(self);
|
||||
await context.VfsCache.Put(self.ToIndexedVirtualFile(), token);
|
||||
return self;
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,7 @@
|
||||
<ProjectReference Include="..\Wabbajack.Hashing.xxHash64\Wabbajack.Hashing.xxHash64.csproj" />
|
||||
<ProjectReference Include="..\Wabbajack.Paths.IO\Wabbajack.Paths.IO.csproj" />
|
||||
<ProjectReference Include="..\Wabbajack.Paths\Wabbajack.Paths.csproj" />
|
||||
<ProjectReference Include="..\Wabbajack.VFS.Interfaces\Wabbajack.VFS.Interfaces.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -141,6 +141,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wabbajack.Downloaders.Manua
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wabbajack.App.Wpf", "Wabbajack.App.Wpf\Wabbajack.App.Wpf.csproj", "{1196560A-9FFD-4290-9418-470508E984C9}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wabbajack.VFS.Interfaces", "Wabbajack.VFS.Interfaces\Wabbajack.VFS.Interfaces.csproj", "{E4BDB22D-11A4-452F-8D10-D9CA9777EA22}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@ -387,6 +389,10 @@ Global
|
||||
{1196560A-9FFD-4290-9418-470508E984C9}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
{1196560A-9FFD-4290-9418-470508E984C9}.Release|Any CPU.ActiveCfg = Release|x64
|
||||
{1196560A-9FFD-4290-9418-470508E984C9}.Release|Any CPU.Build.0 = Release|x64
|
||||
{E4BDB22D-11A4-452F-8D10-D9CA9777EA22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E4BDB22D-11A4-452F-8D10-D9CA9777EA22}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E4BDB22D-11A4-452F-8D10-D9CA9777EA22}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E4BDB22D-11A4-452F-8D10-D9CA9777EA22}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@ -435,6 +441,7 @@ Global
|
||||
{A3813D73-9A8E-4CE7-861A-C59043DFFC14} = {F01F8595-5FD7-4506-8469-F4A5522DACC1}
|
||||
{B10BB6D6-B3FC-4A76-8A07-6A0A0ADDE198} = {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}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {0AA30275-0F38-4A7D-B645-F5505178DDE8}
|
||||
|
Reference in New Issue
Block a user