wabbajack/Wabbajack.VFS/Context.cs

244 lines
8.4 KiB
C#
Raw Normal View History

2021-09-27 12:42:46 +00:00
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Wabbajack.Common;
using Wabbajack.FileExtractor.ExtractedFiles;
using Wabbajack.Hashing.xxHash64;
using Wabbajack.Paths;
using Wabbajack.Paths.IO;
using Wabbajack.RateLimiter;
2022-06-22 01:38:42 +00:00
using Wabbajack.VFS.Interfaces;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
namespace Wabbajack.VFS;
public class Context
2021-09-27 12:42:46 +00:00
{
2021-10-23 16:51:17 +00:00
public const ulong FileVersion = 0x03;
public const string Magic = "WABBAJACK VFS FILE";
private readonly TemporaryFileManager _manager;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
private readonly ParallelOptions _parallelOptions;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
public readonly FileExtractor.FileExtractor Extractor;
public readonly FileHashCache HashCache;
public readonly IResource<Context> Limiter;
2021-10-23 22:27:59 +00:00
public readonly IResource<FileHashCache> HashLimiter;
2021-10-23 16:51:17 +00:00
public readonly ILogger<Context> Logger;
2022-06-22 01:38:42 +00:00
public readonly IVfsCache VfsCache;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
public Context(ILogger<Context> logger, ParallelOptions parallelOptions, TemporaryFileManager manager,
2022-06-22 01:38:42 +00:00
IVfsCache vfsCache,
2021-10-23 22:27:59 +00:00
FileHashCache hashCache, IResource<Context> limiter, IResource<FileHashCache> hashLimiter, FileExtractor.FileExtractor extractor)
2021-10-23 16:51:17 +00:00
{
Limiter = limiter;
2021-10-23 22:27:59 +00:00
HashLimiter = hashLimiter;
2021-10-23 16:51:17 +00:00
Logger = logger;
_manager = manager;
Extractor = extractor;
VfsCache = vfsCache;
HashCache = hashCache;
_parallelOptions = parallelOptions;
}
2021-09-27 12:42:46 +00:00
2022-06-29 13:18:04 +00:00
public Context WithTemporaryFileManager(TemporaryFileManager manager)
{
return new Context(Logger, _parallelOptions, manager, VfsCache, HashCache, Limiter, HashLimiter,
Extractor.WithTemporaryFileManager(manager));
}
2021-10-23 22:27:59 +00:00
2021-10-23 16:51:17 +00:00
public IndexRoot Index { get; private set; } = IndexRoot.Empty;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
public async Task<IndexRoot> AddRoot(AbsolutePath root, CancellationToken token)
{
var filtered = Index.AllFiles.Where(file => file.IsNative && ((AbsolutePath) file.Name).FileExists())
.ToList();
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
var byPath = filtered.ToDictionary(f => f.Name);
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
var filesToIndex = root.EnumerateFiles().Distinct().ToList();
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
var allFiles = await filesToIndex
.PMapAll(async f =>
{
using var job = await Limiter.Begin($"Analyzing {f}", 0, token);
2021-10-23 16:51:17 +00:00
if (byPath.TryGetValue(f, out var found))
if (found.LastModified == f.LastModifiedUtc().AsUnixTime() && found.Size == f.Size())
return found;
2021-09-27 12:42:46 +00:00
try
{
return await VirtualFile.Analyze(this, null, new NativeFileStreamFactory(f), f, token, job: job);
}
catch (Exception ex)
{
Logger.LogError(ex, "While analyzing {File}", f);
throw;
}
2021-10-23 16:51:17 +00:00
}).ToList();
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
var newIndex = await IndexRoot.Empty.Integrate(filtered.Concat(allFiles).ToList());
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
lock (this)
{
Index = newIndex;
2021-09-27 12:42:46 +00:00
}
2021-10-23 16:51:17 +00:00
return newIndex;
}
2021-09-27 12:42:46 +00:00
2022-08-19 23:59:29 +00:00
public async Task<IndexRoot> AddRoots(List<AbsolutePath> roots, CancellationToken token, Func<long, long, Task>? updateFunction = null)
2021-10-23 16:51:17 +00:00
{
var native = Index.AllFiles.Where(file => file.IsNative).ToDictionary(file => file.FullPath.Base);
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
var filtered = Index.AllFiles.Where(file => ((AbsolutePath) file.Name).FileExists()).ToList();
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
var filesToIndex = roots.SelectMany(root => root.EnumerateFiles()).ToList();
2021-09-27 12:42:46 +00:00
2022-08-19 23:59:29 +00:00
var idx = 0;
2021-10-23 16:51:17 +00:00
var allFiles = await filesToIndex
.PMapAll(async f =>
{
2022-08-19 23:59:29 +00:00
using var job = await Limiter.Begin($"Indexing {f.FileName}", 0, token);
2021-10-23 16:51:17 +00:00
if (native.TryGetValue(f, out var found))
if (found.LastModified == f.LastModifiedUtc().AsUnixTime() && found.Size == f.Size())
return found;
2022-08-19 23:59:29 +00:00
Interlocked.Increment(ref idx);
updateFunction?.Invoke(idx, filesToIndex.Count);
2021-10-23 16:51:17 +00:00
return await VirtualFile.Analyze(this, null, new NativeFileStreamFactory(f), f, token);
}).ToList();
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
var newIndex = await IndexRoot.Empty.Integrate(filtered.Concat(allFiles).ToList());
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
lock (this)
{
Index = newIndex;
2021-09-27 12:42:46 +00:00
}
2022-08-19 23:59:29 +00:00
VfsCache.Clean();
2021-10-23 16:51:17 +00:00
return newIndex;
}
/// <summary>
/// Extracts a file
/// </summary>
/// <param name="queue">Work queue to use when required by some formats</param>
/// <param name="files">Predefined list of files to extract, all others will be skipped</param>
/// <param name="callback">Func called for each file extracted</param>
/// <param name="tempFolder">Optional: folder to use for temporary storage</param>
/// <param name="updateTracker">Optional: Status update tracker</param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public async Task Extract(HashSet<VirtualFile> files, Func<VirtualFile, IExtractedFile, ValueTask> callback,
CancellationToken token, AbsolutePath? tempFolder = null)
{
var top = new VirtualFile();
var filesByParent = files.SelectMany(f => f.FilesInFullPath)
.Distinct()
.GroupBy(f => f.Parent ?? top)
.ToDictionary(f => f.Key);
async Task HandleFile(VirtualFile file, IExtractedFile sfn)
2021-09-27 12:42:46 +00:00
{
2021-10-23 16:51:17 +00:00
if (filesByParent.ContainsKey(file))
sfn.CanMove = false;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
if (files.Contains(file)) await callback(file, sfn);
if (filesByParent.TryGetValue(file, out var children))
2021-09-27 12:42:46 +00:00
{
2021-10-23 16:51:17 +00:00
var fileNames = children.ToDictionary(c => c.RelativeName);
try
{
await Extractor.GatheringExtract(sfn,
r => fileNames.ContainsKey(r),
async (rel, csf) =>
{
await HandleFile(fileNames[rel], csf);
return 0;
},
token,
fileNames.Keys.ToHashSet());
}
catch (Exception ex)
2021-09-27 12:42:46 +00:00
{
2021-10-23 16:51:17 +00:00
await using var stream = await sfn.GetStream();
var hash = await stream.HashingCopy(Stream.Null, token);
if (hash != file.Hash)
throw new Exception(
$"File {file.FullPath} is corrupt, please delete it and retry the installation");
throw;
2021-09-27 12:42:46 +00:00
}
}
}
2021-10-23 16:51:17 +00:00
await filesByParent[top].PDoAll(
async file => await HandleFile(file, new ExtractedNativeFile(file.AbsoluteName) {CanMove = false}));
}
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
#region KnownFiles
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
private List<HashRelativePath> _knownFiles = new();
private readonly Dictionary<Hash, AbsolutePath> _knownArchives = new();
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
public void AddKnown(IEnumerable<HashRelativePath> known, Dictionary<Hash, AbsolutePath> archives)
{
_knownFiles.AddRange(known);
foreach (var (key, value) in archives)
_knownArchives.TryAdd(key, value);
}
2021-09-27 12:42:46 +00:00
2021-11-03 05:03:41 +00:00
public async ValueTask BackfillMissing()
2021-10-23 16:51:17 +00:00
{
var newFiles = _knownArchives.ToDictionary(kv => kv.Key,
kv => new VirtualFile
{
Name = kv.Value,
Size = kv.Value.Size(),
Hash = kv.Key
});
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
foreach (var f in newFiles.Values)
f.FillFullPath(0);
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
var parentchild = new Dictionary<(VirtualFile, RelativePath), VirtualFile>();
void BackFillOne(HashRelativePath file)
{
var parent = newFiles[file.Hash];
foreach (var path in file.Parts)
2021-09-27 12:42:46 +00:00
{
2021-10-23 16:51:17 +00:00
if (parentchild.TryGetValue((parent, path), out var foundParent))
2021-09-27 12:42:46 +00:00
{
2021-10-23 16:51:17 +00:00
parent = foundParent;
continue;
2021-09-27 12:42:46 +00:00
}
2021-10-23 16:51:17 +00:00
var nf = new VirtualFile {Name = path, Parent = parent};
nf.FillFullPath();
parent.Children = parent.Children.Add(nf);
parentchild.Add((parent, path), nf);
parent = nf;
}
}
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
_knownFiles.Where(f => f.Parts.Length > 0).Do(BackFillOne);
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
var newIndex = await Index.Integrate(newFiles.Values.ToList());
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
lock (this)
{
Index = newIndex;
2021-09-27 12:42:46 +00:00
}
2021-10-23 16:51:17 +00:00
_knownFiles = new List<HashRelativePath>();
2021-09-27 12:42:46 +00:00
}
2021-10-23 16:51:17 +00:00
#endregion
2021-09-27 12:42:46 +00:00
}