wabbajack/Wabbajack.VFS/VirtualFile.cs

381 lines
11 KiB
C#
Raw Permalink Normal View History

2021-09-27 12:42:46 +00:00
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
2021-09-27 12:42:46 +00:00
using System.Threading;
using System.Threading.Tasks;
2021-09-27 12:42:46 +00:00
using Microsoft.Extensions.Logging;
using Wabbajack.Common;
2021-06-16 05:16:25 +00:00
using Wabbajack.Common.FileSignatures;
2021-09-27 12:42:46 +00:00
using Wabbajack.DTOs.Streams;
using Wabbajack.DTOs.Texture;
2022-06-22 01:38:42 +00:00
using Wabbajack.DTOs.Vfs;
2021-09-27 12:42:46 +00:00
using Wabbajack.Hashing.PHash;
using Wabbajack.Hashing.xxHash64;
using Wabbajack.Paths;
using Wabbajack.Paths.IO;
using Wabbajack.RateLimiter;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
namespace Wabbajack.VFS;
public class VirtualFile
{
2021-10-23 16:51:17 +00:00
private static readonly HashSet<Extension> TextureExtensions = new()
{new Extension(".dds"), new Extension(".tga")};
2021-01-05 22:09:32 +00:00
2021-10-23 16:51:17 +00:00
private static readonly SignatureChecker DDSSig = new(FileType.DDS);
2021-01-05 22:09:32 +00:00
2022-10-07 22:57:12 +00:00
private IEnumerable<VirtualFile>? _thisAndAllChildren;
2020-03-23 12:57:18 +00:00
2021-10-23 16:51:17 +00:00
public IPath Name { get; internal set; }
2021-10-23 16:51:17 +00:00
public RelativePath RelativeName => (RelativePath) Name;
2020-03-24 12:21:19 +00:00
2021-10-23 16:51:17 +00:00
public AbsolutePath AbsoluteName => (AbsolutePath) Name;
2020-03-24 12:21:19 +00:00
2021-10-23 16:51:17 +00:00
public FullPath FullPath { get; private set; }
2021-10-23 16:51:17 +00:00
public Hash Hash { get; internal set; }
public ImageState? ImageState { get; internal set; }
public long Size { get; internal set; }
2021-10-23 16:51:17 +00:00
public ulong LastModified { get; internal set; }
2021-10-23 16:51:17 +00:00
public ulong LastAnalyzed { get; internal set; }
2022-10-07 22:57:12 +00:00
public VirtualFile? Parent { get; internal set; }
2021-10-23 16:51:17 +00:00
public Context Context { get; set; }
2021-10-23 16:51:17 +00:00
/// <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
{
2021-10-23 16:51:17 +00:00
var cnt = 0;
var cur = this;
while (cur != null)
{
2021-10-23 16:51:17 +00:00
cnt += 1;
cur = cur.Parent;
}
2021-10-23 16:51:17 +00:00
return cnt;
}
2021-10-23 16:51:17 +00:00
}
2021-10-23 16:51:17 +00:00
public ImmutableList<VirtualFile> Children { get; internal set; } = ImmutableList<VirtualFile>.Empty;
2021-10-23 16:51:17 +00:00
public bool IsArchive => Children != null && Children.Count > 0;
2021-10-23 16:51:17 +00:00
public bool IsNative => Parent == null;
2021-10-23 16:51:17 +00:00
public IEnumerable<VirtualFile> ThisAndAllChildren
{
get
2019-11-24 23:03:36 +00:00
{
2021-10-23 16:51:17 +00:00
if (_thisAndAllChildren == null)
_thisAndAllChildren = Children.SelectMany(child => child.ThisAndAllChildren).Append(this).ToList();
2019-11-24 23:03:36 +00:00
2021-10-23 16:51:17 +00:00
return _thisAndAllChildren;
2019-11-24 23:03:36 +00:00
}
2021-10-23 16:51:17 +00:00
}
2021-10-23 16:51:17 +00:00
/// <summary>
/// Returns all the virtual files in the path to this file, starting from the root file.
/// </summary>
public IEnumerable<VirtualFile> FilesInFullPath
{
get
{
2021-10-23 16:51:17 +00:00
var stack = ImmutableStack<VirtualFile>.Empty;
var cur = this;
while (cur != null)
{
2021-10-23 16:51:17 +00:00
stack = stack.Push(cur);
cur = cur.Parent;
}
2021-10-23 16:51:17 +00:00
return stack;
}
2021-10-23 16:51:17 +00:00
}
2020-03-24 12:21:19 +00:00
2022-10-07 22:57:12 +00:00
public VirtualFile TopParent => IsNative ? this : Parent!.TopParent;
2021-10-23 16:51:17 +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));
}
2020-03-24 12:21:19 +00:00
2021-10-23 16:51:17 +00:00
public void ThisAndAllChildrenReduced(Action<VirtualFile> fn)
{
fn(this);
foreach (var itm in Children)
itm.ThisAndAllChildrenReduced(fn);
}
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
private static VirtualFile ConvertFromIndexedFile(Context context, IndexedVirtualFile file, IPath path,
VirtualFile vparent, IStreamFactory extractedFile)
{
var vself = new VirtualFile
{
2021-10-23 16:51:17 +00:00
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;
}
2021-01-05 22:09:32 +00:00
2021-10-23 16:51:17 +00:00
internal IndexedVirtualFile ToIndexedVirtualFile()
{
return new IndexedVirtualFile
{
2021-10-23 16:51:17 +00:00
Hash = Hash,
ImageState = ImageState,
Name = Name,
Children = Children.Select(c => c.ToIndexedVirtualFile()).ToList(),
Size = Size
};
}
2021-10-23 16:51:17 +00:00
public static async Task<VirtualFile> Analyze(Context context, VirtualFile? parent,
IStreamFactory extractedFile,
IPath relPath, CancellationToken token, int depth = 0, IJob? job = null)
2021-10-23 16:51:17 +00:00
{
Hash hash;
2021-10-23 22:27:59 +00:00
if (extractedFile is NativeFileStreamFactory)
{
2021-10-23 22:27:59 +00:00
var absPath = (AbsolutePath) extractedFile.Name;
hash = await context.HashCache.FileHashCachedAsync(absPath, token);
}
else
{
await using var hstream = await extractedFile.GetStream();
if (job != null)
job.Size += hstream.Length;
2021-10-23 22:27:59 +00:00
hash = await hstream.HashingCopy(Stream.Null, token, job);
2021-10-23 16:51:17 +00:00
}
2022-07-11 20:55:54 +00:00
var found = await context.VfsCache.Get(hash, extractedFile, token);
2022-06-22 01:38:42 +00:00
if (found != null)
{
var file = ConvertFromIndexedFile(context, found!, relPath, parent!, extractedFile);
file.Name = relPath;
return file;
}
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
await using var stream = await extractedFile.GetStream();
var sig = await FileExtractor.FileExtractor.ArchiveSigs.MatchesAsync(stream);
stream.Position = 0;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
var self = new VirtualFile
{
Context = context,
Name = relPath,
2022-10-07 22:57:12 +00:00
Parent = parent!,
2021-10-23 16:51:17 +00:00
Size = stream.Length,
LastModified = extractedFile.LastModifiedUtc.AsUnixTime(),
LastAnalyzed = DateTime.Now.AsUnixTime(),
Hash = hash
};
2021-10-23 16:51:17 +00:00
if (TextureExtensions.Contains(relPath.FileName.Extension) && await DDSSig.MatchesAsync(stream) != null)
try
{
self.ImageState = await context.ImageLoader.Load(stream);
if (job != null)
{
job.Size += self.Size;
await job.Report((int) self.Size, token);
}
2021-10-23 16:51:17 +00:00
stream.Position = 0;
}
2021-10-23 16:51:17 +00:00
catch (Exception)
{
}
2021-10-23 16:51:17 +00:00
self.FillFullPath(depth);
// Can't extract, so return
if (!sig.HasValue ||
!FileExtractor.FileExtractor.ExtractableExtensions.Contains(relPath.FileName.Extension))
{
2022-06-22 01:38:42 +00:00
await context.VfsCache.Put(self.ToIndexedVirtualFile(), token);
2021-07-05 21:26:30 +00:00
return self;
}
2021-10-23 22:27:59 +00:00
2021-10-23 16:51:17 +00:00
try
2020-04-24 13:56:03 +00:00
{
2021-10-23 16:51:17 +00:00
var list = await context.Extractor.GatheringExtract(extractedFile,
_ => true,
async (path, sfactory) => await Analyze(context, self, sfactory, path, token, depth + 1, job),
2021-10-23 16:51:17 +00:00
token);
2020-04-24 13:56:03 +00:00
2021-10-23 16:51:17 +00:00
self.Children = list.Values.ToImmutableList();
2020-04-24 13:56:03 +00:00
}
2021-10-23 16:51:17 +00:00
catch (EndOfStreamException)
2020-03-24 21:42:28 +00:00
{
2021-10-23 16:51:17 +00:00
return self;
}
2021-10-23 16:51:17 +00:00
catch (Exception ex)
{
Changes for an official Version 3.3.1.0 Release (#2439) * added more visible error messages to avoid user confusion added hard drive free space detection, added red error message text, removed overwrite checkbox, added wiki button link extended the error text for starting wabbajack in protected location removed debug code shortened error message to fit in text box * restored warning removed in error, updated changelog, removed debug includes * Update InstallerVM.cs * Update InstallerVM.cs * Update MainWindowViewModel.cs * added json optional flag to only show version number over modlist image in installer view, if the modlist image already contains the title removed debug code change to pascal case and match existing code style update changelog * Fix manual downloads sometimes launching in browser * Fix manual downloads from secure servers * Remove duplicate user agent code * Create configuration project and performance settings * Bind new performance settings to UI * Use performance settings to limit maximum memory per download * Remove unused settings and related classes * Updated CHANGELOG.md * update CHANGELOG.md * moved the existing files popup to an error message , heralding the return of the overwrite install checkbox * added newline * reverted erroneous edit * gogID for fallout4 added * update CHANGELOG.md * Fix deadlock when loading new settings * change folder/directory check logic * update CHANGELOG.md * revert unnecessary change * update CHANGELOG.md * Bump Wabbajack to .NET 7 * Bump ReactiveUI packages & deps * Update GameFinder to 4.0.0 * Update CHANGELOG.md * Update CHANGELOG.md * Don't try to add cookies if array is empty * Start download from scratch if .download_package can't be parsed * Log when application is shutting down * update CHANGELOG.md * exclude the "SP Consent Message" on nexus from the cleared iframes * Actually use the --outputPath compile option if the user provides one It was just not being used, defaulting to the parent of installPath. it still does, if the user does not specify a directory using --outputPath * update Wabbajack.Compression.BSA dependencies * improve log message to include storage space reference * clearing temp folder when the app closes * update logging message * update CHANGELOG.md * update CHANGELOG.md * update logging for possible exceptions thrown when clearing temp folder * fix cloudflare captcha --------- Co-authored-by: JanuarySnow <bobfordiscord12@gmail.com> Co-authored-by: JanuarySnow <85711747+JanuarySnow@users.noreply.github.com> Co-authored-by: UrbanCMC <UrbanCMC@web.de> Co-authored-by: trawzified <55751269+tr4wzified@users.noreply.github.com> Co-authored-by: Marco Antonio Jaguaribe Costa <marco.antonio.costa@gmail.com> Co-authored-by: Timothy Baldridge <tbaldridge@gmail.com>
2023-11-15 03:19:53 +00:00
context.Logger.LogError(ex, "Error while examining the contents of {Path}", relPath.FileName);
if (!ex.Message.Equals("End of stream before end of limit")) throw;
context.Logger.LogError("Not enough free storage space in {TempFolder}",Environment.CurrentDirectory+"\\temp");
2021-10-23 16:51:17 +00:00
throw;
}
2022-06-22 01:38:42 +00:00
await context.VfsCache.Put(self.ToIndexedVirtualFile(), token);
2021-10-23 16:51:17 +00:00
return self;
}
internal void FillFullPath()
{
var depth = 0;
var self = this;
while (self.Parent != null)
{
2021-10-23 16:51:17 +00:00
depth += 1;
self = self.Parent;
}
2021-10-23 16:51:17 +00:00
FillFullPath(depth);
}
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
internal void FillFullPath(int depth)
{
if (depth == 0)
{
FullPath = new FullPath((AbsolutePath) Name);
2020-03-24 12:21:19 +00:00
}
2021-10-23 16:51:17 +00:00
else
2020-03-24 12:21:19 +00:00
{
2021-10-23 16:51:17 +00:00
var paths = new RelativePath[depth];
var self = this;
for (var idx = depth; idx != 0; idx -= 1)
2020-03-24 12:21:19 +00:00
{
2022-10-07 22:57:12 +00:00
paths[idx - 1] = self!.RelativeName;
2021-10-23 16:51:17 +00:00
self = self.Parent;
2020-03-24 12:21:19 +00:00
}
2021-09-27 12:42:46 +00:00
2022-10-07 22:57:12 +00:00
FullPath = new FullPath(self!.AbsoluteName, paths);
}
2021-10-23 16:51:17 +00:00
}
2021-10-23 16:51:17 +00:00
public void Write(BinaryWriter bw)
{
bw.Write(Name.ToString() ?? string.Empty);
bw.Write(Size);
bw.Write(LastModified);
bw.Write(LastModified);
bw.Write((ulong) Hash);
bw.Write(Children.Count);
foreach (var child in Children)
child.Write(bw);
}
public static VirtualFile Read(Context context, byte[] data)
{
using var ms = new MemoryStream(data);
using var br = new BinaryReader(ms);
return Read(context, null, br);
}
2022-10-07 22:57:12 +00:00
private static VirtualFile Read(Context context, VirtualFile? parent, BinaryReader br)
2021-10-23 16:51:17 +00:00
{
var vf = new VirtualFile
{
Name = br.ReadIPath(),
Size = br.ReadInt64(),
LastModified = br.ReadUInt64(),
LastAnalyzed = br.ReadUInt64(),
Hash = Hash.FromULong(br.ReadUInt64()),
Context = context,
Parent = parent,
Children = ImmutableList<VirtualFile>.Empty
};
vf.FullPath = new FullPath(vf.AbsoluteName);
var children = br.ReadInt32();
for (var i = 0; i < children; i++)
2019-11-15 13:06:34 +00:00
{
2021-10-23 16:51:17 +00:00
var child = Read(context, vf, br, (AbsolutePath) vf.Name, new RelativePath[0]);
vf.Children = vf.Children.Add(child);
}
2019-11-15 13:06:34 +00:00
2021-10-23 16:51:17 +00:00
return vf;
}
2020-03-24 12:21:19 +00:00
2021-10-23 16:51:17 +00:00
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 = Hash.FromULong(br.ReadUInt64()),
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);
2019-11-15 13:06:34 +00:00
}
2021-10-23 16:51:17 +00:00
return vf;
}
public HashRelativePath MakeRelativePaths()
{
var paths = new RelativePath[FilesInFullPath.Count() - 1];
var idx = 0;
foreach (var itm in FilesInFullPath.Skip(1))
{
2021-10-23 16:51:17 +00:00
paths[idx] = (RelativePath) itm.Name;
idx += 1;
}
2021-10-23 16:51:17 +00:00
var path = new HashRelativePath(FilesInFullPath.First().Hash, paths);
return path;
}
2022-10-07 22:57:12 +00:00
public VirtualFile? InSameFolder(RelativePath relativePath)
2021-10-23 16:51:17 +00:00
{
var newPath = FullPath.InSameFolder(relativePath);
return Context.Index.ByFullPath.TryGetValue(newPath, out var found) ? found : null;
}
2021-09-27 12:42:46 +00:00
}