mirror of
https://github.com/wabbajack-tools/wabbajack.git
synced 2024-08-30 18:42:17 +00:00
Merge branch 'main' into gamefinder-fix
This commit is contained in:
commit
14f7369bc1
7
.github/workflows/tests.yaml
vendored
7
.github/workflows/tests.yaml
vendored
@ -39,12 +39,7 @@ jobs:
|
|||||||
include-prerelease: true
|
include-prerelease: true
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:EnableWindowsTargeting=true --filter "Category!=FlakeyNetwork"
|
run: dotnet test /p:EnableWindowsTargeting=true --filter "Category!=FlakeyNetwork"
|
||||||
|
|
||||||
- uses: codecov/codecov-action@v3
|
|
||||||
with:
|
|
||||||
flags: unittests, ${{runner.os}}
|
|
||||||
verbose: true
|
|
||||||
|
|
||||||
#- name: Upload Test Folder on Failure
|
#- name: Upload Test Folder on Failure
|
||||||
# if: ${{ failure() }}
|
# if: ${{ failure() }}
|
||||||
|
12
CHANGELOG.md
12
CHANGELOG.md
@ -1,8 +1,18 @@
|
|||||||
### Changelog
|
### Changelog
|
||||||
|
|
||||||
#### Version - 3.0.2.4 - TBD
|
#### Version - 3.0.4.0 - TBD
|
||||||
* upgrade GameFinder to 2.2.1
|
* upgrade GameFinder to 2.2.1
|
||||||
|
|
||||||
|
#### Version - 3.0.3.1 - 10/30/2022
|
||||||
|
* Fix file verification issues for CreatedBSAs
|
||||||
|
* Fix files during verification where CreatedDate > LastModified
|
||||||
|
|
||||||
|
#### Version - 3.0.3.0 - 10/26/2022
|
||||||
|
* Verify hashes of all installed files
|
||||||
|
* Verify contents of BSAs during installation
|
||||||
|
* Provide a new CLI command for verifying a installed modlist
|
||||||
|
* When downloading from one Nexus CDN server fails, WJ will now try alternate Nexus servers
|
||||||
|
|
||||||
#### Version - 3.0.2.3 - 10/19/2022
|
#### Version - 3.0.2.3 - 10/19/2022
|
||||||
* HOTFIX: revert GameFinder library to 1.8 until it's a bit more forgiving of corrupt files
|
* HOTFIX: revert GameFinder library to 1.8 until it's a bit more forgiving of corrupt files
|
||||||
|
|
||||||
|
@ -134,7 +134,7 @@ namespace Wabbajack
|
|||||||
var assembly = Assembly.GetExecutingAssembly();
|
var assembly = Assembly.GetExecutingAssembly();
|
||||||
var location = assembly.Location;
|
var location = assembly.Location;
|
||||||
if (string.IsNullOrWhiteSpace(location))
|
if (string.IsNullOrWhiteSpace(location))
|
||||||
location = Process.GetCurrentProcess().MainModule?.FileName ?? "";
|
location = Process.GetCurrentProcess().MainModule?.FileName ?? throw new Exception("Assembly location is unavailable!");
|
||||||
|
|
||||||
_logger.LogInformation("App Location: {Location}", assembly.Location);
|
_logger.LogInformation("App Location: {Location}", assembly.Location);
|
||||||
var fvi = FileVersionInfo.GetVersionInfo(location);
|
var fvi = FileVersionInfo.GetVersionInfo(location);
|
||||||
@ -149,10 +149,9 @@ namespace Wabbajack
|
|||||||
// setup file association
|
// setup file association
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var applicationRegistrationService =
|
var applicationRegistrationService = _serviceProvider.GetRequiredService<IApplicationRegistrationService>();
|
||||||
_serviceProvider.GetRequiredService<IApplicationRegistrationService>();
|
|
||||||
|
|
||||||
var applicationInfo = new ApplicationInfo(assembly);
|
var applicationInfo = new ApplicationInfo("Wabbajack", "Wabbajack", "Wabbajack", location);
|
||||||
applicationInfo.SupportedExtensions.Add("wabbajack");
|
applicationInfo.SupportedExtensions.Add("wabbajack");
|
||||||
applicationRegistrationService.RegisterApplication(applicationInfo);
|
applicationRegistrationService.RegisterApplication(applicationInfo);
|
||||||
}
|
}
|
||||||
|
@ -51,6 +51,8 @@ CommandLineBuilder.RegisterCommand<UploadToNexus>(UploadToNexus.Definition, c =>
|
|||||||
services.AddSingleton<UploadToNexus>();
|
services.AddSingleton<UploadToNexus>();
|
||||||
CommandLineBuilder.RegisterCommand<ValidateLists>(ValidateLists.Definition, c => ((ValidateLists)c).Run);
|
CommandLineBuilder.RegisterCommand<ValidateLists>(ValidateLists.Definition, c => ((ValidateLists)c).Run);
|
||||||
services.AddSingleton<ValidateLists>();
|
services.AddSingleton<ValidateLists>();
|
||||||
|
CommandLineBuilder.RegisterCommand<VerifyModlistInstall>(VerifyModlistInstall.Definition, c => ((VerifyModlistInstall)c).Run);
|
||||||
|
services.AddSingleton<VerifyModlistInstall>();
|
||||||
CommandLineBuilder.RegisterCommand<VFSIndex>(VFSIndex.Definition, c => ((VFSIndex)c).Run);
|
CommandLineBuilder.RegisterCommand<VFSIndex>(VFSIndex.Definition, c => ((VFSIndex)c).Run);
|
||||||
services.AddSingleton<VFSIndex>();
|
services.AddSingleton<VFSIndex>();
|
||||||
}
|
}
|
||||||
|
@ -70,7 +70,7 @@ public class ModlistReport
|
|||||||
|
|
||||||
string FixupTo(RelativePath path)
|
string FixupTo(RelativePath path)
|
||||||
{
|
{
|
||||||
if (path.GetPart(0) != StandardInstaller.BSACreationDir.ToString()) return path.ToString();
|
if (path.GetPart(0) != Consts.BSACreationDir.ToString()) return path.ToString();
|
||||||
var bsaId = path.GetPart(1);
|
var bsaId = path.GetPart(1);
|
||||||
|
|
||||||
if (!bsas.TryGetValue(bsaId, out var bsa))
|
if (!bsas.TryGetValue(bsaId, out var bsa))
|
||||||
|
173
Wabbajack.CLI/Verbs/VerifyModlistInstall.cs
Normal file
173
Wabbajack.CLI/Verbs/VerifyModlistInstall.cs
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Wabbajack.CLI.Builder;
|
||||||
|
using Wabbajack.Common;
|
||||||
|
using Wabbajack.Compression.BSA;
|
||||||
|
using Wabbajack.DTOs;
|
||||||
|
using Wabbajack.DTOs.BSA.FileStates;
|
||||||
|
using Wabbajack.DTOs.Directives;
|
||||||
|
using Wabbajack.DTOs.JsonConverters;
|
||||||
|
using Wabbajack.Hashing.xxHash64;
|
||||||
|
using Wabbajack.Installer;
|
||||||
|
using Wabbajack.Paths;
|
||||||
|
using Wabbajack.Paths.IO;
|
||||||
|
using Wabbajack.RateLimiter;
|
||||||
|
using Wabbajack.VFS;
|
||||||
|
using AbsolutePathExtensions = Wabbajack.Common.AbsolutePathExtensions;
|
||||||
|
|
||||||
|
namespace Wabbajack.CLI.Verbs;
|
||||||
|
|
||||||
|
public class VerifyModlistInstall
|
||||||
|
{
|
||||||
|
private readonly DTOSerializer _dtos;
|
||||||
|
private readonly ILogger<VerifyModlistInstall> _logger;
|
||||||
|
|
||||||
|
public VerifyModlistInstall(ILogger<VerifyModlistInstall> logger, DTOSerializer dtos, IResource<FileHashCache> limiter)
|
||||||
|
{
|
||||||
|
_limiter = limiter;
|
||||||
|
_logger = logger;
|
||||||
|
_dtos = dtos;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static VerbDefinition Definition = new("verify-modlist-install", "Verify a modlist installed correctly",
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
new OptionDefinition(typeof(AbsolutePath), "m", "modlistLocation",
|
||||||
|
"The .wabbajack file used to install the modlist"),
|
||||||
|
new OptionDefinition(typeof(AbsolutePath), "i", "installFolder", "The installation folder of the modlist")
|
||||||
|
});
|
||||||
|
|
||||||
|
private readonly IResource<FileHashCache> _limiter;
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<int> Run(AbsolutePath modlistLocation, AbsolutePath installFolder, CancellationToken token)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Loading modlist {ModList}", modlistLocation);
|
||||||
|
var list = await StandardInstaller.LoadFromFile(_dtos, modlistLocation);
|
||||||
|
|
||||||
|
_logger.LogInformation("Indexing files");
|
||||||
|
var byTo = list.Directives.ToDictionary(d => d.To);
|
||||||
|
|
||||||
|
|
||||||
|
_logger.LogInformation("Scanning files");
|
||||||
|
var errors = await list.Directives.PMapAllBatchedAsync(_limiter, async directive =>
|
||||||
|
{
|
||||||
|
if (!(directive is CreateBSA || directive.IsDeterministic))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (directive.To.InFolder(Consts.BSACreationDir))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var dest = directive.To.RelativeTo(installFolder);
|
||||||
|
if (!dest.FileExists())
|
||||||
|
{
|
||||||
|
return new Result
|
||||||
|
{
|
||||||
|
Path = directive.To,
|
||||||
|
Message = $"File does not exist directive {directive.GetType()}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Consts.KnownModifiedFiles.Contains(directive.To.FileName))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (directive is CreateBSA bsa)
|
||||||
|
{
|
||||||
|
return await VerifyBSA(dest, bsa, byTo, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dest.Size() != directive.Size)
|
||||||
|
{
|
||||||
|
return new Result
|
||||||
|
{
|
||||||
|
Path = directive.To,
|
||||||
|
Message = $"Sizes do not match got {dest.Size()} expected {directive.Size}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (directive.Size > (1024 * 1024 * 128))
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Hashing {Size} file at {Path}", directive.Size.ToFileSizeString(),
|
||||||
|
directive.To);
|
||||||
|
}
|
||||||
|
|
||||||
|
var hash = await AbsolutePathExtensions.Hash(dest, token);
|
||||||
|
if (hash != directive.Hash)
|
||||||
|
{
|
||||||
|
return new Result
|
||||||
|
{
|
||||||
|
Path = directive.To,
|
||||||
|
Message = $"Hashes do not match, got {hash} expected {directive.Hash}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}).Where(r => r != null)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
_logger.LogInformation("Found {Count} errors", errors.Count);
|
||||||
|
|
||||||
|
|
||||||
|
foreach (var error in errors)
|
||||||
|
{
|
||||||
|
_logger.LogError("{File} | {Message}", error.Path, error.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Result?> VerifyBSA(AbsolutePath dest, CreateBSA bsa, Dictionary<RelativePath, Directive> byTo, CancellationToken token)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Verifying Created BSA {To}", bsa.To);
|
||||||
|
var archive = await BSADispatch.Open(dest);
|
||||||
|
var filesIndexed = archive.Files.ToDictionary(d => d.Path);
|
||||||
|
|
||||||
|
if (dest.Extension == Ext.Bsa && dest.Size() >= 1024L * 1024 * 1024 * 2)
|
||||||
|
{
|
||||||
|
return new Result()
|
||||||
|
{
|
||||||
|
Path = bsa.To,
|
||||||
|
Message = $"BSA is over 2GB in size, this will cause crashes : {bsa.To}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var file in bsa.FileStates)
|
||||||
|
{
|
||||||
|
if (file is BA2DX10File) continue;
|
||||||
|
var state = filesIndexed[file.Path];
|
||||||
|
var sf = await state.GetStreamFactory(token);
|
||||||
|
await using var stream = await sf.GetStream();
|
||||||
|
var hash = await stream.Hash(token);
|
||||||
|
|
||||||
|
var astate = bsa.FileStates.First(f => f.Path == state.Path);
|
||||||
|
var srcDirective = byTo[Consts.BSACreationDir.Combine(bsa.TempID, astate.Path)];
|
||||||
|
|
||||||
|
if (!srcDirective.IsDeterministic)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (srcDirective.Hash != hash)
|
||||||
|
{
|
||||||
|
return new Result
|
||||||
|
{
|
||||||
|
Path = bsa.To,
|
||||||
|
Message =
|
||||||
|
$"BSA {bsa.To} contents do not match at {file.Path} got {hash} expected {srcDirective.Hash}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Result
|
||||||
|
{
|
||||||
|
public RelativePath Path { get; set; }
|
||||||
|
public string Message { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -21,4 +21,25 @@ public static class AbsolutePathExtensions
|
|||||||
await using var fs = path.Open(FileMode.Open, FileAccess.Read, FileShare.Read);
|
await using var fs = path.Open(FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||||
return await fs.FromJson<T>(dtos);
|
return await fs.FromJson<T>(dtos);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async Task<Hash> WriteAllHashedAsync(this AbsolutePath file, Stream srcStream, CancellationToken token,
|
||||||
|
bool closeWhenDone = true)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var dest = file.Open(FileMode.Create, FileAccess.Write, FileShare.None);
|
||||||
|
return await srcStream.HashingCopy(dest, token);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (closeWhenDone)
|
||||||
|
srcStream.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<Hash> WriteAllHashedAsync(this AbsolutePath file, byte[] data, CancellationToken token)
|
||||||
|
{
|
||||||
|
await using var dest = file.Open(FileMode.Create, FileAccess.Write, FileShare.None);
|
||||||
|
return await new MemoryStream(data).HashingCopy(dest, token);
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
using Wabbajack.Hashing.xxHash64;
|
using Wabbajack.Hashing.xxHash64;
|
||||||
using Wabbajack.Paths;
|
using Wabbajack.Paths;
|
||||||
|
|
||||||
@ -12,4 +13,6 @@ public abstract class Directive
|
|||||||
/// location the file will be copied to, relative to the install path.
|
/// location the file will be copied to, relative to the install path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public RelativePath To { get; set; }
|
public RelativePath To { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore] public virtual bool IsDeterministic => true;
|
||||||
}
|
}
|
@ -8,4 +8,6 @@ namespace Wabbajack.DTOs.Directives;
|
|||||||
public class ArchiveMeta : Directive
|
public class ArchiveMeta : Directive
|
||||||
{
|
{
|
||||||
public RelativePath SourceDataID { get; set; }
|
public RelativePath SourceDataID { get; set; }
|
||||||
|
|
||||||
|
public override bool IsDeterministic => false;
|
||||||
}
|
}
|
@ -13,4 +13,6 @@ public class CreateBSA : Directive
|
|||||||
public RelativePath TempID { get; set; }
|
public RelativePath TempID { get; set; }
|
||||||
public IArchive State { get; set; }
|
public IArchive State { get; set; }
|
||||||
public AFile[] FileStates { get; set; } = Array.Empty<AFile>();
|
public AFile[] FileStates { get; set; } = Array.Empty<AFile>();
|
||||||
|
|
||||||
|
public override bool IsDeterministic => false;
|
||||||
}
|
}
|
@ -9,4 +9,5 @@ namespace Wabbajack.DTOs.Directives;
|
|||||||
[JsonAlias("RemappedInlineFile, Wabbajack.Lib")]
|
[JsonAlias("RemappedInlineFile, Wabbajack.Lib")]
|
||||||
public class RemappedInlineFile : InlineFile
|
public class RemappedInlineFile : InlineFile
|
||||||
{
|
{
|
||||||
|
public override bool IsDeterministic => false;
|
||||||
}
|
}
|
@ -11,4 +11,5 @@ public class TransformedTexture : FromArchive
|
|||||||
/// The file to apply to the source file to patch it
|
/// The file to apply to the source file to patch it
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ImageState ImageState { get; set; } = new();
|
public ImageState ImageState { get; set; } = new();
|
||||||
|
public override bool IsDeterministic => false;
|
||||||
}
|
}
|
@ -9,7 +9,6 @@ namespace Wabbajack.Downloaders.Dispatcher.Test;
|
|||||||
|
|
||||||
public class VerificationCacheTests
|
public class VerificationCacheTests
|
||||||
{
|
{
|
||||||
private readonly TemporaryFileManager _temp;
|
|
||||||
private readonly ILogger<VerificationCache.VerificationCache> _logger;
|
private readonly ILogger<VerificationCache.VerificationCache> _logger;
|
||||||
|
|
||||||
public VerificationCacheTests(ILogger<VerificationCache.VerificationCache> logger)
|
public VerificationCacheTests(ILogger<VerificationCache.VerificationCache> logger)
|
||||||
|
@ -40,7 +40,8 @@ public class MediaFireDownloader : ADownloader<DTOs.DownloadStates.MediaFire>, I
|
|||||||
|
|
||||||
public override bool IsAllowed(ServerAllowList allowList, IDownloadState state)
|
public override bool IsAllowed(ServerAllowList allowList, IDownloadState state)
|
||||||
{
|
{
|
||||||
return true;
|
var mediaFireState = (DTOs.DownloadStates.MediaFire) state;
|
||||||
|
return allowList.AllowedPrefixes.Any(p => mediaFireState.Url.ToString().StartsWith(p, StringComparison.OrdinalIgnoreCase));
|
||||||
}
|
}
|
||||||
|
|
||||||
public override IDownloadState? Resolve(IReadOnlyDictionary<string, string> iniData)
|
public override IDownloadState? Resolve(IReadOnlyDictionary<string, string> iniData)
|
||||||
|
@ -129,9 +129,24 @@ public class NexusDownloader : ADownloader<Nexus>, IUrlDownloader
|
|||||||
var urls = await _api.DownloadLink(state.Game.MetaData().NexusName!, state.ModID, state.FileID, token);
|
var urls = await _api.DownloadLink(state.Game.MetaData().NexusName!, state.ModID, state.FileID, token);
|
||||||
_logger.LogInformation("Downloading Nexus File: {game}|{modid}|{fileid}", state.Game, state.ModID,
|
_logger.LogInformation("Downloading Nexus File: {game}|{modid}|{fileid}", state.Game, state.ModID,
|
||||||
state.FileID);
|
state.FileID);
|
||||||
var message = new HttpRequestMessage(HttpMethod.Get, urls.info.First().URI);
|
foreach (var link in urls.info)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var message = new HttpRequestMessage(HttpMethod.Get, link.URI);
|
||||||
return await _downloader.Download(message, destination, job, token);
|
return await _downloader.Download(message, destination, job, token);
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (link.URI == urls.info.Last().URI)
|
||||||
|
throw;
|
||||||
|
_logger.LogInformation(ex, "While downloading {URI}, trying another link", link.URI);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should never be hit
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
catch (HttpRequestException ex)
|
catch (HttpRequestException ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "While downloading from the Nexus {Message}", ex.Message);
|
_logger.LogError(ex, "While downloading from the Nexus {Message}", ex.Message);
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Wabbajack.Common;
|
||||||
using Wabbajack.DTOs.Streams;
|
using Wabbajack.DTOs.Streams;
|
||||||
|
using Wabbajack.Hashing.xxHash64;
|
||||||
using Wabbajack.Paths;
|
using Wabbajack.Paths;
|
||||||
|
|
||||||
namespace Wabbajack.FileExtractor.ExtractedFiles;
|
namespace Wabbajack.FileExtractor.ExtractedFiles;
|
||||||
@ -16,4 +18,23 @@ public interface IExtractedFile : IStreamFactory
|
|||||||
/// <param name="newPath">destination to move the entry to</param>
|
/// <param name="newPath">destination to move the entry to</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public ValueTask Move(AbsolutePath newPath, CancellationToken token);
|
public ValueTask Move(AbsolutePath newPath, CancellationToken token);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class IExtractedFileExtensions
|
||||||
|
{
|
||||||
|
public static async Task<Hash> MoveHashedAsync(this IExtractedFile file, AbsolutePath destPath, CancellationToken token)
|
||||||
|
{
|
||||||
|
if (file.CanMove)
|
||||||
|
{
|
||||||
|
await file.Move(destPath, token);
|
||||||
|
return await destPath.Hash(token);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await using var s = await file.GetStream();
|
||||||
|
return await destPath.WriteAllHashedAsync(s, token, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -12,9 +12,11 @@ using Wabbajack.Common;
|
|||||||
using Wabbajack.Downloaders;
|
using Wabbajack.Downloaders;
|
||||||
using Wabbajack.Downloaders.GameFile;
|
using Wabbajack.Downloaders.GameFile;
|
||||||
using Wabbajack.DTOs;
|
using Wabbajack.DTOs;
|
||||||
|
using Wabbajack.DTOs.BSA.FileStates;
|
||||||
using Wabbajack.DTOs.Directives;
|
using Wabbajack.DTOs.Directives;
|
||||||
using Wabbajack.DTOs.DownloadStates;
|
using Wabbajack.DTOs.DownloadStates;
|
||||||
using Wabbajack.DTOs.JsonConverters;
|
using Wabbajack.DTOs.JsonConverters;
|
||||||
|
using Wabbajack.FileExtractor.ExtractedFiles;
|
||||||
using Wabbajack.Hashing.PHash;
|
using Wabbajack.Hashing.PHash;
|
||||||
using Wabbajack.Hashing.xxHash64;
|
using Wabbajack.Hashing.xxHash64;
|
||||||
using Wabbajack.Installer.Utilities;
|
using Wabbajack.Installer.Utilities;
|
||||||
@ -39,7 +41,7 @@ public abstract class AInstaller<T>
|
|||||||
where T : AInstaller<T>
|
where T : AInstaller<T>
|
||||||
{
|
{
|
||||||
private const int _limitMS = 100;
|
private const int _limitMS = 100;
|
||||||
public static RelativePath BSACreationDir = "TEMP_BSA_FILES".ToRelativePath();
|
|
||||||
private static readonly Regex NoDeleteRegex = new(@"(?i)[\\\/]\[NoDelete\]", RegexOptions.Compiled);
|
private static readonly Regex NoDeleteRegex = new(@"(?i)[\\\/]\[NoDelete\]", RegexOptions.Compiled);
|
||||||
|
|
||||||
protected readonly InstallerConfiguration _configuration;
|
protected readonly InstallerConfiguration _configuration;
|
||||||
@ -243,7 +245,8 @@ public abstract class AInstaller<T>
|
|||||||
await using var patchDataStream = await InlinedFileStream(pfa.PatchID);
|
await using var patchDataStream = await InlinedFileStream(pfa.PatchID);
|
||||||
{
|
{
|
||||||
await using var os = destPath.Open(FileMode.Create, FileAccess.ReadWrite, FileShare.None);
|
await using var os = destPath.Open(FileMode.Create, FileAccess.ReadWrite, FileShare.None);
|
||||||
await BinaryPatching.ApplyPatch(s, patchDataStream, os);
|
var hash = await BinaryPatching.ApplyPatch(s, patchDataStream, os);
|
||||||
|
ThrowOnNonMatchingHash(file, hash);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -263,12 +266,14 @@ public abstract class AInstaller<T>
|
|||||||
case FromArchive _:
|
case FromArchive _:
|
||||||
if (grouped[vf].Count() == 1)
|
if (grouped[vf].Count() == 1)
|
||||||
{
|
{
|
||||||
await sf.Move(destPath, token);
|
var hash = await sf.MoveHashedAsync(destPath, token);
|
||||||
|
ThrowOnNonMatchingHash(file, hash);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await using var s = await sf.GetStream();
|
await using var s = await sf.GetStream();
|
||||||
await destPath.WriteAllAsync(s, token, false);
|
var hash = await destPath.WriteAllHashedAsync(s, token, false);
|
||||||
|
ThrowOnNonMatchingHash(file, hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@ -282,6 +287,25 @@ public abstract class AInstaller<T>
|
|||||||
}, token);
|
}, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void ThrowOnNonMatchingHash(Directive file, Hash gotHash)
|
||||||
|
{
|
||||||
|
if (file.Hash != gotHash)
|
||||||
|
ThrowNonMatchingError(file, gotHash);
|
||||||
|
}
|
||||||
|
private void ThrowNonMatchingError(Directive file, Hash gotHash)
|
||||||
|
{
|
||||||
|
_logger.LogError("Hashes for {Path} did not match, expected {Expected} got {Got}", file.To, file.Hash, gotHash);
|
||||||
|
throw new Exception($"Hashes for {file.To} did not match, expected {file.Hash} got {gotHash}");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected void ThrowOnNonMatchingHash(CreateBSA bsa, Directive directive, AFile state, Hash hash)
|
||||||
|
{
|
||||||
|
if (hash == directive.Hash) return;
|
||||||
|
_logger.LogError("Hashes for BSA don't match after extraction, {BSA}, {Directive}, {ExpectedHash}, {Hash}", bsa.To, directive.To, directive.Hash, hash);
|
||||||
|
throw new Exception($"Hashes for {bsa.To} file {directive.To} did not match, expected {directive.Hash} got {hash}");
|
||||||
|
}
|
||||||
|
|
||||||
public async Task DownloadArchives(CancellationToken token)
|
public async Task DownloadArchives(CancellationToken token)
|
||||||
{
|
{
|
||||||
var missing = ModList.Archives.Where(a => !HashedArchives.ContainsKey(a.Hash)).ToList();
|
var missing = ModList.Archives.Where(a => !HashedArchives.ContainsKey(a.Hash)).ToList();
|
||||||
@ -474,8 +498,8 @@ public abstract class AInstaller<T>
|
|||||||
return d switch
|
return d switch
|
||||||
{
|
{
|
||||||
CreateBSA bsa => !bsasToNotBuild.Contains(bsa.TempID),
|
CreateBSA bsa => !bsasToNotBuild.Contains(bsa.TempID),
|
||||||
FromArchive a when a.To.StartsWith($"{BSACreationDir}") => !bsasToNotBuild.Any(b =>
|
FromArchive a when a.To.StartsWith($"{Consts.BSACreationDir}") => !bsasToNotBuild.Any(b =>
|
||||||
a.To.RelativeTo(_configuration.Install).InFolder(_configuration.Install.Combine(BSACreationDir, b))),
|
a.To.RelativeTo(_configuration.Install).InFolder(_configuration.Install.Combine(Consts.BSACreationDir, b))),
|
||||||
_ => true
|
_ => true
|
||||||
};
|
};
|
||||||
}).ToDictionary(d => d.To);
|
}).ToDictionary(d => d.To);
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using Wabbajack.Paths;
|
using Wabbajack.Paths;
|
||||||
|
|
||||||
namespace Wabbajack.Installer;
|
namespace Wabbajack.Installer;
|
||||||
@ -26,4 +28,11 @@ public static class Consts
|
|||||||
public const string StepDownloading = "Downloading";
|
public const string StepDownloading = "Downloading";
|
||||||
public const string StepHashing = "Hashing";
|
public const string StepHashing = "Hashing";
|
||||||
public const string StepFinished = "Finished";
|
public const string StepFinished = "Finished";
|
||||||
|
public static RelativePath BSACreationDir = "TEMP_BSA_FILES".ToRelativePath();
|
||||||
|
|
||||||
|
public static HashSet<RelativePath> KnownModifiedFiles = new[]
|
||||||
|
{
|
||||||
|
"modlist.txt",
|
||||||
|
"SkyrimPrefs.ini"
|
||||||
|
}.Select(r => r.ToRelativePath()).ToHashSet();
|
||||||
}
|
}
|
@ -19,9 +19,11 @@ using Wabbajack.Compression.Zip;
|
|||||||
using Wabbajack.Downloaders;
|
using Wabbajack.Downloaders;
|
||||||
using Wabbajack.Downloaders.GameFile;
|
using Wabbajack.Downloaders.GameFile;
|
||||||
using Wabbajack.DTOs;
|
using Wabbajack.DTOs;
|
||||||
|
using Wabbajack.DTOs.BSA.FileStates;
|
||||||
using Wabbajack.DTOs.Directives;
|
using Wabbajack.DTOs.Directives;
|
||||||
using Wabbajack.DTOs.DownloadStates;
|
using Wabbajack.DTOs.DownloadStates;
|
||||||
using Wabbajack.DTOs.JsonConverters;
|
using Wabbajack.DTOs.JsonConverters;
|
||||||
|
using Wabbajack.Hashing.xxHash64;
|
||||||
using Wabbajack.Installer.Utilities;
|
using Wabbajack.Installer.Utilities;
|
||||||
using Wabbajack.Networking.WabbajackClientApi;
|
using Wabbajack.Networking.WabbajackClientApi;
|
||||||
using Wabbajack.Paths;
|
using Wabbajack.Paths;
|
||||||
@ -271,6 +273,8 @@ public class StandardInstaller : AInstaller<StandardInstaller>
|
|||||||
private async Task BuildBSAs(CancellationToken token)
|
private async Task BuildBSAs(CancellationToken token)
|
||||||
{
|
{
|
||||||
var bsas = ModList.Directives.OfType<CreateBSA>().ToList();
|
var bsas = ModList.Directives.OfType<CreateBSA>().ToList();
|
||||||
|
_logger.LogInformation("Generating debug caches");
|
||||||
|
var indexedByDestination = UnoptimizedDirectives.ToDictionary(d => d.To);
|
||||||
_logger.LogInformation("Building {bsasCount} bsa files", bsas.Count);
|
_logger.LogInformation("Building {bsasCount} bsa files", bsas.Count);
|
||||||
NextStep("Installing", "Building BSAs", bsas.Count);
|
NextStep("Installing", "Building BSAs", bsas.Count);
|
||||||
|
|
||||||
@ -278,34 +282,50 @@ public class StandardInstaller : AInstaller<StandardInstaller>
|
|||||||
{
|
{
|
||||||
UpdateProgress(1);
|
UpdateProgress(1);
|
||||||
_logger.LogInformation("Building {bsaTo}", bsa.To.FileName);
|
_logger.LogInformation("Building {bsaTo}", bsa.To.FileName);
|
||||||
var sourceDir = _configuration.Install.Combine(BSACreationDir, bsa.TempID);
|
var sourceDir = _configuration.Install.Combine(Consts.BSACreationDir, bsa.TempID);
|
||||||
|
|
||||||
await using var a = BSADispatch.CreateBuilder(bsa.State, _manager);
|
await using var a = BSADispatch.CreateBuilder(bsa.State, _manager);
|
||||||
var streams = await bsa.FileStates.PMapAll(async state =>
|
var streams = await bsa.FileStates.PMapAllBatchedAsync(_limiter, async state =>
|
||||||
{
|
{
|
||||||
using var job = await _limiter.Begin($"Adding {state.Path.FileName}", 0, token);
|
|
||||||
var fs = sourceDir.Combine(state.Path).Open(FileMode.Open, FileAccess.Read, FileShare.Read);
|
var fs = sourceDir.Combine(state.Path).Open(FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||||
var size = fs.Length;
|
|
||||||
job.Size = size;
|
|
||||||
await a.AddFile(state, fs, token);
|
await a.AddFile(state, fs, token);
|
||||||
await job.Report((int)size, token);
|
|
||||||
return fs;
|
return fs;
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
_logger.LogInformation("Writing {bsaTo}", bsa.To);
|
_logger.LogInformation("Writing {bsaTo}", bsa.To);
|
||||||
var outPath = _configuration.Install.Combine(bsa.To);
|
var outPath = _configuration.Install.Combine(bsa.To);
|
||||||
await using var outStream = outPath.Open(FileMode.Create, FileAccess.Write, FileShare.None);
|
|
||||||
|
await using (var outStream = outPath.Open(FileMode.Create, FileAccess.Write, FileShare.None))
|
||||||
|
{
|
||||||
await a.Build(outStream, token);
|
await a.Build(outStream, token);
|
||||||
|
}
|
||||||
|
|
||||||
streams.Do(s => s.Dispose());
|
streams.Do(s => s.Dispose());
|
||||||
|
|
||||||
await FileHashCache.FileHashWriteCache(outPath, bsa.Hash);
|
await FileHashCache.FileHashWriteCache(outPath, bsa.Hash);
|
||||||
sourceDir.DeleteDirectory();
|
sourceDir.DeleteDirectory();
|
||||||
|
|
||||||
|
_logger.LogInformation("Verifying {bsaTo}", bsa.To);
|
||||||
|
var reader = await BSADispatch.Open(outPath);
|
||||||
|
var results = await reader.Files.PMapAllBatchedAsync(_limiter, async state =>
|
||||||
|
{
|
||||||
|
var sf = await state.GetStreamFactory(token);
|
||||||
|
await using var stream = await sf.GetStream();
|
||||||
|
var hash = await stream.Hash(token);
|
||||||
|
|
||||||
|
var astate = bsa.FileStates.First(f => f.Path == state.Path);
|
||||||
|
var srcDirective = indexedByDestination[Consts.BSACreationDir.Combine(bsa.TempID, astate.Path)];
|
||||||
|
//DX10Files are lossy
|
||||||
|
if (astate is not BA2DX10File && srcDirective.IsDeterministic)
|
||||||
|
ThrowOnNonMatchingHash(bsa, srcDirective, astate, hash);
|
||||||
|
return (srcDirective, hash);
|
||||||
|
}).ToHashSet();
|
||||||
}
|
}
|
||||||
|
|
||||||
var bsaDir = _configuration.Install.Combine(BSACreationDir);
|
var bsaDir = _configuration.Install.Combine(Consts.BSACreationDir);
|
||||||
if (bsaDir.DirectoryExists())
|
if (bsaDir.DirectoryExists())
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Removing temp folder {bsaCreationDir}", BSACreationDir);
|
_logger.LogInformation("Removing temp folder {bsaCreationDir}", Consts.BSACreationDir);
|
||||||
bsaDir.DeleteDirectory();
|
bsaDir.DeleteDirectory();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -329,7 +349,10 @@ public class StandardInstaller : AInstaller<StandardInstaller>
|
|||||||
await FileHashCache.FileHashCachedAsync(outPath, token);
|
await FileHashCache.FileHashCachedAsync(outPath, token);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
await outPath.WriteAllBytesAsync(await LoadBytesFromPath(directive.SourceDataID), token);
|
var hash = await outPath.WriteAllHashedAsync(await LoadBytesFromPath(directive.SourceDataID), token);
|
||||||
|
if (!Consts.KnownModifiedFiles.Contains(directive.To.FileName))
|
||||||
|
ThrowOnNonMatchingHash(directive, hash);
|
||||||
|
|
||||||
await FileHashCache.FileHashWriteCache(outPath, directive.Hash);
|
await FileHashCache.FileHashWriteCache(outPath, directive.Hash);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -481,7 +504,8 @@ public class StandardInstaller : AInstaller<StandardInstaller>
|
|||||||
.Open(FileMode.Create, FileAccess.ReadWrite, FileShare.None);
|
.Open(FileMode.Create, FileAccess.ReadWrite, FileShare.None);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await BinaryPatching.ApplyPatch(new MemoryStream(srcData), new MemoryStream(patchData), fs);
|
var hash = await BinaryPatching.ApplyPatch(new MemoryStream(srcData), new MemoryStream(patchData), fs);
|
||||||
|
ThrowOnNonMatchingHash(m, hash);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
@ -1,16 +1,19 @@
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Octodiff.Core;
|
using Octodiff.Core;
|
||||||
using Octodiff.Diagnostics;
|
using Octodiff.Diagnostics;
|
||||||
|
using Wabbajack.Hashing.xxHash64;
|
||||||
|
|
||||||
namespace Wabbajack.Installer.Utilities;
|
namespace Wabbajack.Installer.Utilities;
|
||||||
|
|
||||||
public class BinaryPatching
|
public class BinaryPatching
|
||||||
{
|
{
|
||||||
public static ValueTask ApplyPatch(Stream input, Stream deltaStream, Stream output)
|
public static async ValueTask<Hash> ApplyPatch(Stream input, Stream deltaStream, Stream output, CancellationToken? token = null)
|
||||||
{
|
{
|
||||||
var deltaApplier = new DeltaApplier();
|
var deltaApplier = new DeltaApplier();
|
||||||
deltaApplier.Apply(input, new BinaryDeltaReader(deltaStream, new NullProgressReporter()), output);
|
deltaApplier.Apply(input, new BinaryDeltaReader(deltaStream, new NullProgressReporter()), output);
|
||||||
return ValueTask.CompletedTask;
|
output.Position = 0;
|
||||||
|
return await output.Hash(token ?? CancellationToken.None);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ini-parser-netstandard" Version="2.5.2" />
|
<PackageReference Include="ini-parser-netstandard" Version="2.5.2" />
|
||||||
<PackageReference Include="Octodiff" Version="1.3.23" />
|
<PackageReference Include="Octopus.Octodiff" Version="1.3.93" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -33,8 +33,7 @@ public class NexusApi
|
|||||||
private (ValidateInfo info, ResponseMetadata header) _lastValidatedInfo;
|
private (ValidateInfo info, ResponseMetadata header) _lastValidatedInfo;
|
||||||
|
|
||||||
public NexusApi(ITokenProvider<NexusApiState> apiKey, ILogger<NexusApi> logger, HttpClient client,
|
public NexusApi(ITokenProvider<NexusApiState> apiKey, ILogger<NexusApi> logger, HttpClient client,
|
||||||
IResource<HttpClient> limiter,
|
IResource<HttpClient> limiter, ApplicationInfo appInfo, JsonSerializerOptions jsonOptions)
|
||||||
ApplicationInfo appInfo, JsonSerializerOptions jsonOptions)
|
|
||||||
{
|
{
|
||||||
ApiKey = apiKey;
|
ApiKey = apiKey;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
@ -56,6 +56,11 @@ public static class AbsolutePathExtensions
|
|||||||
return new FileInfo(file.ToNativePath()).LastWriteTimeUtc;
|
return new FileInfo(file.ToNativePath()).LastWriteTimeUtc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static DateTime CreatedUtc(this AbsolutePath file)
|
||||||
|
{
|
||||||
|
return new FileInfo(file.ToNativePath()).CreationTimeUtc;
|
||||||
|
}
|
||||||
|
|
||||||
public static DateTime LastModified(this AbsolutePath file)
|
public static DateTime LastModified(this AbsolutePath file)
|
||||||
{
|
{
|
||||||
return new FileInfo(file.ToNativePath()).LastWriteTime;
|
return new FileInfo(file.ToNativePath()).LastWriteTime;
|
||||||
|
@ -92,6 +92,10 @@ public class FileHashCache
|
|||||||
if (result == default || result.Hash == default)
|
if (result == default || result.Hash == default)
|
||||||
return default;
|
return default;
|
||||||
|
|
||||||
|
// Fix for strange issue where dates are messed up on some systems
|
||||||
|
if (file.LastModifiedUtc() < file.CreatedUtc())
|
||||||
|
file.Touch();
|
||||||
|
|
||||||
if (result.LastModified == file.LastModifiedUtc().ToFileTimeUtc())
|
if (result.LastModified == file.LastModifiedUtc().ToFileTimeUtc())
|
||||||
{
|
{
|
||||||
return result.Hash;
|
return result.Hash;
|
||||||
|
Loading…
Reference in New Issue
Block a user