wabbajack/Wabbajack.VirtualFileSystem/Context.cs

387 lines
14 KiB
C#
Raw Normal View History

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Text;
using System.Threading.Tasks;
using Alphaleonis.Win32.Filesystem;
using Wabbajack.Common;
using Wabbajack.Common.CSP;
using Directory = Alphaleonis.Win32.Filesystem.Directory;
using File = System.IO.File;
using FileInfo = Alphaleonis.Win32.Filesystem.FileInfo;
using Path = Alphaleonis.Win32.Filesystem.Path;
namespace Wabbajack.VirtualFileSystem
{
public class Context
{
2020-01-10 13:16:41 +00:00
static Context()
{
Utils.Log("Cleaning VFS, this may take a bit of time");
2020-03-23 12:57:18 +00:00
Utils.DeleteDirectory(StagingFolder);
2020-01-10 13:16:41 +00:00
}
public const ulong FileVersion = 0x03;
public const string Magic = "WABBAJACK VFS FILE";
2020-03-23 12:57:18 +00:00
private static readonly AbsolutePath StagingFolder = ((RelativePath)"vfs_staging").RelativeToWorkingDirectory();
public IndexRoot Index { get; private set; } = IndexRoot.Empty;
/// <summary>
/// A stream of tuples of ("Update Title", 0.25) which represent the name of the current task
/// and the current progress.
/// </summary>
public IObservable<(string, float)> ProgressUpdates => _progressUpdates;
private readonly Subject<(string, float)> _progressUpdates = new Subject<(string, float)>();
2019-11-17 04:16:42 +00:00
public StatusUpdateTracker UpdateTracker { get; set; } = new StatusUpdateTracker(1);
public WorkQueue Queue { get; }
public bool UseExtendedHashes { get; set; }
public Context(WorkQueue queue, bool extendedHashes = false)
2019-11-17 04:16:42 +00:00
{
Queue = queue;
UseExtendedHashes = extendedHashes;
2019-11-17 04:16:42 +00:00
}
2020-03-23 12:57:18 +00:00
public static TemporaryDirectory GetTemporaryFolder()
{
2020-03-23 12:57:18 +00:00
return new TemporaryDirectory(((RelativePath)Guid.NewGuid().ToString()).RelativeTo(StagingFolder));
}
2020-03-23 12:57:18 +00:00
public async Task<IndexRoot> AddRoot(AbsolutePath root)
{
2020-03-23 12:57:18 +00:00
var filtered = Index.AllFiles.Where(file => file.IsNative && ((AbsolutePath) file.Name).Exists).ToList();
var byPath = filtered.ToImmutableDictionary(f => f.Name);
2020-03-23 12:57:18 +00:00
var filesToIndex = root.EnumerateFiles().Distinct().ToList();
var results = Channel.Create(1024, ProgressUpdater<VirtualFile>($"Indexing {root}", filesToIndex.Count));
var allFiles = await filesToIndex
.PMap(Queue, async f =>
2019-11-16 00:01:37 +00:00
{
if (byPath.TryGetValue(f, out var found))
{
2020-03-23 12:57:18 +00:00
if (found.LastModified == f.LastModifiedUtc.AsUnixTime() && found.Size == f.Size)
2019-11-16 00:01:37 +00:00
return found;
}
2020-03-24 21:42:28 +00:00
return await VirtualFile.Analyze(this, null, f, f, 0);
2019-11-16 00:01:37 +00:00
});
2019-12-07 02:54:27 +00:00
var newIndex = await IndexRoot.Empty.Integrate(filtered.Concat(allFiles).ToList());
lock (this)
{
Index = newIndex;
}
return newIndex;
}
2020-03-23 12:57:18 +00:00
public async Task<IndexRoot> AddRoots(List<AbsolutePath> roots)
{
2020-03-23 12:57:18 +00:00
var native = Index.AllFiles.Where(file => file.IsNative).ToDictionary(file => file.StagedPath);
2020-03-23 12:57:18 +00:00
var filtered = Index.AllFiles.Where(file => ((AbsolutePath)file.Name).Exists).ToList();
2020-03-23 12:57:18 +00:00
var filesToIndex = roots.SelectMany(root => root.EnumerateFiles()).ToList();
var allFiles = await filesToIndex
.PMap(Queue, async f =>
{
2020-03-23 12:57:18 +00:00
Utils.Status($"Indexing {Path.GetFileName((string)f)}");
if (native.TryGetValue(f, out var found))
{
2020-03-23 12:57:18 +00:00
if (found.LastModified == f.LastModifiedUtc.AsUnixTime() && found.Size == f.Size)
return found;
}
2020-03-24 21:42:28 +00:00
return await VirtualFile.Analyze(this, null, f, f, 0);
});
2019-12-07 02:54:27 +00:00
var newIndex = await IndexRoot.Empty.Integrate(filtered.Concat(allFiles).ToList());
lock (this)
{
Index = newIndex;
}
return newIndex;
}
class Box<T>
{
public T Value { get; set; }
}
private Func<IObservable<T>, IObservable<T>> ProgressUpdater<T>(string s, float totalCount)
{
if (totalCount == 0)
totalCount = 1;
var box = new Box<float>();
return sub => sub.Select(itm =>
{
box.Value += 1;
_progressUpdates.OnNext((s, box.Value / totalCount));
return itm;
});
}
public async Task WriteToFile(AbsolutePath filename)
{
await using var fs = filename.Create();
await using var bw = new BinaryWriter(fs, Encoding.UTF8, true);
fs.SetLength(0);
bw.Write(Encoding.ASCII.GetBytes(Magic));
bw.Write(FileVersion);
bw.Write((ulong) Index.AllFiles.Count);
(await Index.AllFiles
2019-11-17 04:16:42 +00:00
.PMap(Queue, f =>
{
var ms = new MemoryStream();
2020-03-24 12:21:19 +00:00
using var ibw = new BinaryWriter(ms, Encoding.UTF8, true);
f.Write(ibw);
return ms;
}))
.Do(ms =>
{
var size = ms.Position;
ms.Position = 0;
bw.Write((ulong) size);
ms.CopyTo(fs);
});
Utils.Log($"Wrote {fs.Position.ToFileSizeString()} file as vfs cache file {filename}");
}
public async Task IntegrateFromFile(AbsolutePath filename)
{
try
{
await using var fs = filename.OpenRead();
using var br = new BinaryReader(fs, Encoding.UTF8, true);
var magic = Encoding.ASCII.GetString(br.ReadBytes(Encoding.ASCII.GetBytes(Magic).Length));
var fileVersion = br.ReadUInt64();
if (fileVersion != FileVersion || magic != Magic)
throw new InvalidDataException("Bad Data Format");
var numFiles = br.ReadUInt64();
var files = Enumerable.Range(0, (int) numFiles)
.Select(idx =>
{
var size = br.ReadUInt64();
var bytes = new byte[size];
br.BaseStream.Read(bytes, 0, (int) size);
return VirtualFile.Read(this, bytes);
}).ToList();
var newIndex = await Index.Integrate(files);
lock (this)
{
Index = newIndex;
}
}
catch (IOException)
{
filename.Delete();
}
}
public async Task<Action> Stage(IEnumerable<VirtualFile> files)
{
var grouped = files.SelectMany(f => f.FilesInFullPath)
.Distinct()
.Where(f => f.Parent != null)
.GroupBy(f => f.Parent)
.OrderBy(f => f.Key?.NestingFactor ?? 0)
.ToList();
var paths = new List<AbsolutePath>();
foreach (var group in grouped)
{
var tmpPath = ((RelativePath)Guid.NewGuid().ToString()).RelativeTo(StagingFolder);
await FileExtractor.ExtractAll(Queue, group.Key.StagedPath, tmpPath);
paths.Add(tmpPath);
foreach (var file in group)
file.StagedPath = file.RelativeName.RelativeTo(tmpPath);
}
return () =>
{
paths.Do(p =>
{
p.DeleteDirectory();
});
};
}
public async Task<DisposableList<VirtualFile>> StageWith(IEnumerable<VirtualFile> files)
2019-11-15 13:06:34 +00:00
{
return new DisposableList<VirtualFile>(await Stage(files), files);
2019-11-15 13:06:34 +00:00
}
2019-11-15 13:37:04 +00:00
#region KnownFiles
private List<HashRelativePath> _knownFiles = new List<HashRelativePath>();
2020-03-25 12:47:25 +00:00
private Dictionary<Hash, AbsolutePath> _knownArchives = new Dictionary<Hash, AbsolutePath>();
public void AddKnown(IEnumerable<HashRelativePath> known, Dictionary<Hash, AbsolutePath> archives)
2019-11-15 13:06:34 +00:00
{
2019-11-15 13:37:04 +00:00
_knownFiles.AddRange(known);
2020-03-25 12:47:25 +00:00
foreach (var (key, value) in archives)
_knownArchives.TryAdd(key, value);
2019-11-15 13:06:34 +00:00
}
2019-12-07 02:54:27 +00:00
public async Task BackfillMissing()
2019-11-15 13:06:34 +00:00
{
var newFiles = _knownFiles.Where(f => f.Paths.Length == 0)
.GroupBy(f => f.BaseHash)
2019-11-15 13:37:04 +00:00
.ToDictionary(f => f.Key, s => new VirtualFile()
{
Name = s.First().Paths[0],
Hash = s.First().BaseHash,
2019-11-15 13:37:04 +00:00
Context = this
});
var parentchild = new Dictionary<(VirtualFile, RelativePath), VirtualFile>();
2019-11-15 13:37:04 +00:00
void BackFillOne(HashRelativePath file)
2019-11-15 13:37:04 +00:00
{
var parent = newFiles[file.BaseHash];
2019-11-15 13:37:04 +00:00
foreach (var path in file.Paths.Skip(1))
{
if (parentchild.TryGetValue((parent, path), out var foundParent))
{
parent = foundParent;
continue;
}
var nf = new VirtualFile {Name = path, Parent = parent};
2019-11-15 13:37:04 +00:00
parent.Children = parent.Children.Add(nf);
parentchild.Add((parent, path), nf);
parent = nf;
}
}
_knownFiles.Where(f => f.Paths.Length > 1).Do(BackFillOne);
2019-12-07 02:54:27 +00:00
var newIndex = await Index.Integrate(newFiles.Values.ToList());
2019-11-15 13:37:04 +00:00
lock (this)
Index = newIndex;
_knownFiles = new List<HashRelativePath>();
2019-11-15 13:37:04 +00:00
2019-11-15 13:06:34 +00:00
}
2019-11-15 13:37:04 +00:00
#endregion
2019-11-15 13:06:34 +00:00
}
public class DisposableList<T> : List<T>, IDisposable
{
private Action _unstage;
public DisposableList(Action unstage, IEnumerable<T> files) : base(files)
{
_unstage = unstage;
}
public void Dispose()
{
_unstage();
}
}
public class IndexRoot
{
public static IndexRoot Empty = new IndexRoot();
public IndexRoot(ImmutableList<VirtualFile> aFiles,
2020-03-24 21:42:28 +00:00
Dictionary<FullPath, VirtualFile> byFullPath,
2020-03-22 15:50:53 +00:00
ImmutableDictionary<Hash, ImmutableStack<VirtualFile>> byHash,
2020-03-23 12:57:18 +00:00
ImmutableDictionary<AbsolutePath, VirtualFile> byRoot,
2020-03-24 12:21:19 +00:00
ImmutableDictionary<IPath, ImmutableStack<VirtualFile>> byName)
{
AllFiles = aFiles;
ByFullPath = byFullPath;
ByHash = byHash;
ByRootPath = byRoot;
2019-11-15 13:06:34 +00:00
ByName = byName;
}
public IndexRoot()
{
AllFiles = ImmutableList<VirtualFile>.Empty;
2020-03-24 21:42:28 +00:00
ByFullPath = new Dictionary<FullPath, VirtualFile>();
2020-03-22 15:50:53 +00:00
ByHash = ImmutableDictionary<Hash, ImmutableStack<VirtualFile>>.Empty;
2020-03-23 12:57:18 +00:00
ByRootPath = ImmutableDictionary<AbsolutePath, VirtualFile>.Empty;
2020-03-24 12:21:19 +00:00
ByName = ImmutableDictionary<IPath, ImmutableStack<VirtualFile>>.Empty;
}
2019-11-15 13:06:34 +00:00
public ImmutableList<VirtualFile> AllFiles { get; }
2020-03-24 21:42:28 +00:00
public Dictionary<FullPath, VirtualFile> ByFullPath { get; }
2020-03-22 15:50:53 +00:00
public ImmutableDictionary<Hash, ImmutableStack<VirtualFile>> ByHash { get; }
2020-03-24 12:21:19 +00:00
public ImmutableDictionary<IPath, ImmutableStack<VirtualFile>> ByName { get; set; }
2020-03-23 12:57:18 +00:00
public ImmutableDictionary<AbsolutePath, VirtualFile> ByRootPath { get; }
2019-12-07 02:54:27 +00:00
public async Task<IndexRoot> Integrate(ICollection<VirtualFile> files)
{
Utils.Log($"Integrating {files.Count} files");
var allFiles = AllFiles.Concat(files).GroupBy(f => f.Name).Select(g => g.Last()).ToImmutableList();
var byFullPath = Task.Run(() => allFiles.SelectMany(f => f.ThisAndAllChildren)
2020-03-24 21:42:28 +00:00
.ToDictionary(f => f.FullPath));
var byHash = Task.Run(() => allFiles.SelectMany(f => f.ThisAndAllChildren)
2020-03-22 15:50:53 +00:00
.Where(f => f.Hash != Hash.Empty)
.ToGroupedImmutableDictionary(f => f.Hash));
var byName = Task.Run(() => allFiles.SelectMany(f => f.ThisAndAllChildren)
.ToGroupedImmutableDictionary(f => f.Name));
2019-11-15 13:06:34 +00:00
2020-03-24 12:21:19 +00:00
var byRootPath = Task.Run(() => allFiles.ToImmutableDictionary(f => f.AbsoluteName));
var result = new IndexRoot(allFiles,
2019-12-07 02:54:27 +00:00
await byFullPath,
await byHash,
await byRootPath,
await byName);
Utils.Log($"Done integrating");
return result;
2019-11-15 13:06:34 +00:00
}
2020-03-23 12:57:18 +00:00
public VirtualFile FileForArchiveHashPath(HashRelativePath argArchiveHashPath)
2019-11-15 13:06:34 +00:00
{
2020-03-23 12:57:18 +00:00
var cur = ByHash[argArchiveHashPath.BaseHash].First(f => f.Parent == null);
return argArchiveHashPath.Paths.Aggregate(cur, (current, itm) => ByName[itm].First(f => f.Parent == current));
}
}
public class TemporaryDirectory : IDisposable
{
2020-03-23 12:57:18 +00:00
public TemporaryDirectory(AbsolutePath name)
{
FullName = name;
2020-03-23 12:57:18 +00:00
if (!FullName.Exists)
FullName.CreateDirectory();
}
2020-03-23 12:57:18 +00:00
public AbsolutePath FullName { get; }
public void Dispose()
{
2020-03-23 12:57:18 +00:00
if (FullName.Exists)
Utils.DeleteDirectory(FullName);
}
}
}