wabbajack/Wabbajack.VirtualFileSystem/VirtualFile.cs

446 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.Net.Http;
using System.Threading.Tasks;
using K4os.Hash.Crc;
using RocksDbSharp;
using Wabbajack.Common;
namespace Wabbajack.VirtualFileSystem
{
public class VirtualFile
{
private static RocksDb _vfsCache;
static VirtualFile()
{
var options = new DbOptions().SetCreateIfMissing(true);
_vfsCache = RocksDb.Open(options, (string)Consts.LocalAppDataPath.Combine("GlobalVFSCache.rocksDb"));
}
2020-03-23 12:57:18 +00:00
private AbsolutePath _stagedPath;
2020-03-24 12:21:19 +00:00
private IEnumerable<VirtualFile> _thisAndAllChildren;
2020-03-23 12:57:18 +00:00
2020-03-24 12:21:19 +00:00
public IPath Name { get; internal set; }
2020-03-24 12:21:19 +00:00
public RelativePath RelativeName => (RelativePath)Name;
public AbsolutePath AbsoluteName => (AbsolutePath)Name;
public FullPath FullPath { get; private set; }
2020-03-22 15:50:53 +00:00
public Hash Hash { get; internal set; }
2020-03-24 12:21:19 +00:00
public ExtendedHashes ExtendedHashes { get; set; }
public long Size { get; internal set; }
2020-03-23 12:57:18 +00:00
public ulong LastModified { get; internal set; }
2020-03-23 12:57:18 +00:00
public ulong LastAnalyzed { get; internal set; }
public VirtualFile Parent { get; internal set; }
public Context Context { get; set; }
private IExtractedFile _stagedFile = null;
public IExtractedFile StagedFile
{
get
{
if (IsNative) return new ExtractedDiskFile(AbsoluteName);
if (_stagedFile == null)
throw new InvalidDataException("File is unstaged");
return _stagedFile;
}
set
{
_stagedFile = value;
}
}
/// <summary>
/// Returns the nesting factor for this file. Native files will have a nesting of 1, the factor
/// goes up for each nesting of a file in an archive.
/// </summary>
public int NestingFactor
{
get
{
var cnt = 0;
var cur = this;
while (cur != null)
{
cnt += 1;
cur = cur.Parent;
}
return cnt;
}
}
public ImmutableList<VirtualFile> Children { get; internal set; } = ImmutableList<VirtualFile>.Empty;
public bool IsArchive => Children != null && Children.Count > 0;
public bool IsNative => Parent == null;
2019-11-24 23:03:36 +00:00
public IEnumerable<VirtualFile> ThisAndAllChildren
{
get
{
if (_thisAndAllChildren == null)
{
_thisAndAllChildren = Children.SelectMany(child => child.ThisAndAllChildren).Append(this).ToList();
}
return _thisAndAllChildren;
}
}
/// <summary>
/// Returns all the virtual files in the path to this file, starting from the root file.
/// </summary>
public IEnumerable<VirtualFile> FilesInFullPath
{
get
{
var stack = ImmutableStack<VirtualFile>.Empty;
var cur = this;
while (cur != null)
{
stack = stack.Push(cur);
cur = cur.Parent;
}
return stack;
}
}
2020-03-24 12:21:19 +00:00
public T ThisAndAllChildrenReduced<T>(T acc, Func<T, VirtualFile, T> fn)
{
acc = fn(acc, this);
return Children.Aggregate(acc, (current, itm) => itm.ThisAndAllChildrenReduced(current, fn));
}
public void ThisAndAllChildrenReduced(Action<VirtualFile> fn)
{
fn(this);
foreach (var itm in Children)
itm.ThisAndAllChildrenReduced(fn);
}
private static VirtualFile ConvertFromIndexedFile(Context context, IndexedVirtualFile file, IPath path, VirtualFile vparent, IExtractedFile 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
};
vself.FillFullPath();
vself.Children = file.Children.Select(f => ConvertFromIndexedFile(context, f, f.Name, vself, extractedFile)).ToImmutableList();
return vself;
}
private static bool TryGetFromCache(Context context, VirtualFile parent, IPath path, IExtractedFile extractedFile, Hash hash, out VirtualFile found)
{
var result = _vfsCache.Get(hash.ToArray());
if (result == null)
{
found = null;
return false;
}
var data = new MemoryStream(result).FromJson<IndexedVirtualFile>();
found = ConvertFromIndexedFile(context, data, path, parent, extractedFile);
return true;
}
private IndexedVirtualFile ToIndexedVirtualFile()
{
return new IndexedVirtualFile
{
Hash = Hash,
Name = Name,
Children = Children.Select(c => c.ToIndexedVirtualFile()).ToList(),
Size = Size
};
}
2020-03-24 12:21:19 +00:00
public static async Task<VirtualFile> Analyze(Context context, VirtualFile parent, IExtractedFile extractedFile,
2020-03-24 21:42:28 +00:00
IPath relPath, int depth = 0)
{
var hash = await extractedFile.HashAsync();
if (!context.UseExtendedHashes && FileExtractor.MightBeArchive(relPath.FileName.Extension))
{
var result = await TryGetContentsFromServer(hash);
if (result != null)
{
Utils.Log($"Downloaded VFS data for {relPath.FileName}");
2020-03-24 12:21:19 +00:00
return ConvertFromIndexedFile(context, result, relPath, parent, extractedFile);
}
}
if (TryGetFromCache(context, parent, relPath, extractedFile, hash, out var vself))
return vself;
var self = new VirtualFile
{
Context = context,
2020-03-23 12:57:18 +00:00
Name = relPath,
Parent = parent,
Size = extractedFile.Size,
LastModified = extractedFile.LastModifiedUtc.AsUnixTime(),
2020-03-23 12:57:18 +00:00
LastAnalyzed = DateTime.Now.AsUnixTime(),
Hash = hash
};
2020-03-24 21:42:28 +00:00
self.FillFullPath(depth);
if (context.UseExtendedHashes)
self.ExtendedHashes = await ExtendedHashes.FromFile(extractedFile);
2019-11-16 00:01:37 +00:00
if (!await extractedFile.CanExtract()) return self;
try
{
await using var extracted = await extractedFile.ExtractAll(context.Queue);
var list = await extracted
.PMap(context.Queue,
file => Analyze(context, self, file.Value, file.Key, depth + 1));
self.Children = list.ToImmutableList();
}
catch (Exception ex)
{
Utils.Log($"Error while examining the contents of {relPath.FileName}");
throw;
}
await using var ms = new MemoryStream();
self.ToIndexedVirtualFile().ToJson(ms);
_vfsCache.Put(self.Hash.ToArray(), ms.ToArray());
return self;
}
2020-04-24 13:56:03 +00:00
internal void FillFullPath()
{
int depth = 0;
var self = this;
while (self.Parent != null)
{
depth += 1;
self = self.Parent;
}
FillFullPath(depth);
}
internal void FillFullPath(int depth)
2020-03-24 21:42:28 +00:00
{
if (depth == 0)
{
2020-04-24 13:56:03 +00:00
FullPath = new FullPath((AbsolutePath)Name);
2020-03-24 21:42:28 +00:00
}
else
{
var paths = new RelativePath[depth];
var self = this;
for (var idx = depth; idx != 0; idx -= 1)
{
paths[idx - 1] = self.RelativeName;
self = self.Parent;
}
FullPath = new FullPath(self.AbsoluteName, paths);
}
}
2020-03-22 15:50:53 +00:00
private static async Task<IndexedVirtualFile> TryGetContentsFromServer(Hash hash)
{
try
{
var client = new HttpClient();
2020-03-24 12:21:19 +00:00
var response =
await client.GetAsync($"http://{Consts.WabbajackCacheHostname}/indexed_files/{hash.ToHex()}");
if (!response.IsSuccessStatusCode)
return null;
using (var stream = await response.Content.ReadAsStreamAsync())
{
return stream.FromJson<IndexedVirtualFile>();
}
}
catch (Exception)
{
return null;
}
}
2020-03-24 12:21:19 +00:00
public void Write(BinaryWriter bw)
{
2020-03-24 12:21:19 +00:00
bw.Write(Name);
bw.Write(Size);
bw.Write(LastModified);
bw.Write(LastModified);
bw.Write(Hash);
bw.Write(Children.Count);
foreach (var child in Children)
child.Write(bw);
}
public static VirtualFile Read(Context context, byte[] data)
{
2020-03-23 12:57:18 +00:00
using var ms = new MemoryStream(data);
2020-03-24 12:21:19 +00:00
using var br = new BinaryReader(ms);
return Read(context, null, br);
}
2020-03-24 12:21:19 +00:00
private static VirtualFile Read(Context context, VirtualFile parent, BinaryReader br)
{
2020-03-24 12:21:19 +00:00
var vf = new VirtualFile
{
Name = br.ReadIPath(),
Size = br.ReadInt64(),
LastModified = br.ReadUInt64(),
LastAnalyzed = br.ReadUInt64(),
Hash = br.ReadHash(),
Context = context,
Parent = parent,
Children = ImmutableList<VirtualFile>.Empty
};
2020-03-24 21:42:28 +00:00
vf.FullPath = new FullPath(vf.AbsoluteName, new RelativePath[0]);
2020-03-24 12:21:19 +00:00
var children = br.ReadInt32();
for (var i = 0; i < children; i++)
{
var child = Read(context, vf, br, (AbsolutePath)vf.Name, new RelativePath[0]);
vf.Children = vf.Children.Add(child);
}
return vf;
}
private static VirtualFile Read(Context context, VirtualFile parent, BinaryReader br, AbsolutePath top, RelativePath[] subpaths)
{
var name = (RelativePath)br.ReadIPath();
subpaths = subpaths.Add(name);
var vf = new VirtualFile
{
Name = name,
Size = br.ReadInt64(),
LastModified = br.ReadUInt64(),
LastAnalyzed = br.ReadUInt64(),
Hash = br.ReadHash(),
Context = context,
Parent = parent,
Children = ImmutableList<VirtualFile>.Empty,
FullPath = new FullPath(top, subpaths)
};
var children = br.ReadInt32();
for (var i = 0; i < children; i++)
{
var child = Read(context, vf, br,top, subpaths);
vf.Children = vf.Children.Add(child);
}
return vf;
}
public HashRelativePath MakeRelativePaths()
2019-11-15 13:06:34 +00:00
{
2020-03-24 12:21:19 +00:00
var paths = new RelativePath[FilesInFullPath.Count() - 1];
2019-11-15 13:06:34 +00:00
var idx = 0;
2019-11-15 13:06:34 +00:00
foreach (var itm in FilesInFullPath.Skip(1))
{
2020-03-24 12:21:19 +00:00
paths[idx] = (RelativePath)itm.Name;
2019-11-15 13:06:34 +00:00
idx += 1;
}
2020-03-24 12:21:19 +00:00
var path = new HashRelativePath(FilesInFullPath.First().Hash, paths);
2019-11-15 13:06:34 +00:00
return path;
}
2019-11-15 13:41:08 +00:00
public async ValueTask<Stream> OpenRead()
2019-11-15 13:41:08 +00:00
{
return await StagedFile.OpenRead();
2019-11-15 13:41:08 +00:00
}
}
public class ExtendedHashes
{
2020-03-24 12:21:19 +00:00
public string SHA256 { get; set; }
public string SHA1 { get; set; }
public string MD5 { get; set; }
public string CRC { get; set; }
public static async ValueTask<ExtendedHashes> FromFile(IExtractedFile file)
{
var hashes = new ExtendedHashes();
await using var stream = await file.OpenRead();
2020-04-24 13:56:03 +00:00
hashes.SHA256 = System.Security.Cryptography.SHA256.Create().ComputeHash(stream).ToHex();
stream.Position = 0;
hashes.SHA1 = System.Security.Cryptography.SHA1.Create().ComputeHash(stream).ToHex();
stream.Position = 0;
hashes.MD5 = System.Security.Cryptography.MD5.Create().ComputeHash(stream).ToHex();
stream.Position = 0;
var bytes = new byte[1024 * 8];
var crc = new Crc32();
while (true)
{
2020-04-24 13:56:03 +00:00
var read = stream.Read(bytes, 0, bytes.Length);
if (read == 0) break;
crc.Update(bytes, 0, read);
}
2020-04-24 13:56:03 +00:00
hashes.CRC = crc.DigestBytes().ToHex();
return hashes;
}
}
public class CannotStageNativeFile : Exception
{
public CannotStageNativeFile(string cannotStageANativeFile) : base(cannotStageANativeFile)
{
}
}
public class UnstagedFileException : Exception
{
2020-03-23 12:57:18 +00:00
private readonly FullPath _fullPath;
2020-03-23 12:57:18 +00:00
public UnstagedFileException(FullPath fullPath) : base($"File {fullPath} is unstaged, cannot get staged name")
{
_fullPath = fullPath;
}
}
2019-11-24 23:03:36 +00:00
}