CVR Manager 1.0.0

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Exterrata
2026-10-25 02:46:35 -04:00
commit 9bb4f85802
59 changed files with 7913 additions and 0 deletions
+169
View File
@@ -0,0 +1,169 @@
using System.Diagnostics;
using CVRManager.Models;
namespace CVRManager.Services;
public enum CacheState { Missing, Shared, Local, LinkedElsewhere }
public static class CacheService
{
public static CacheState GetState(InstallInfo install)
{
var path = Paths.CacheDir(install);
var info = new DirectoryInfo(path);
if (!info.Exists) return CacheState.Missing;
if (!info.Attributes.HasFlag(FileAttributes.ReparsePoint)) return CacheState.Local;
var target = info.LinkTarget;
return target != null && SamePath(target, Paths.SharedCache) ? CacheState.Shared : CacheState.LinkedElsewhere;
}
static bool SamePath(string a, string b) =>
string.Equals(Path.GetFullPath(a).TrimEnd('\\'), Path.GetFullPath(b).TrimEnd('\\'), StringComparison.OrdinalIgnoreCase);
public static Task LinkAsync(InstallInfo install, Action<string> log, CancellationToken ct) => Task.Run(() =>
{
var path = Paths.CacheDir(install);
Directory.CreateDirectory(Paths.SharedCache);
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
switch (GetState(install))
{
case CacheState.Shared:
return;
case CacheState.Local:
MergeInto(path, Paths.SharedCache, log, ct);
if (Directory.EnumerateFileSystemEntries(path).Any())
throw new IOException($"Cache folder still has content after merge: {path}");
Directory.Delete(path);
break;
case CacheState.LinkedElsewhere:
Directory.Delete(path, false);
break;
}
CreateJunction(path, Paths.SharedCache);
log("Cache now points at the shared cache.");
}, ct);
public static void MergeInto(string source, string shared, Action<string> log, CancellationToken ct)
{
Directory.CreateDirectory(shared);
var moved = 0;
var dupes = 0;
var sameRoot = string.Equals(Path.GetPathRoot(Path.GetFullPath(source)), Path.GetPathRoot(Path.GetFullPath(shared)), StringComparison.OrdinalIgnoreCase);
foreach (var entry in Directory.EnumerateFileSystemEntries(source).ToList())
{
ct.ThrowIfCancellationRequested();
var name = Path.GetFileName(entry);
var dest = Path.Combine(shared, name);
var isDir = Directory.Exists(entry);
if (!(isDir ? Directory.Exists(dest) : File.Exists(dest)))
{
if (isDir)
{
if (sameRoot) Directory.Move(entry, dest);
else
{
CopyTree(entry, dest, ct);
Directory.Delete(entry, true);
}
}
else File.Move(entry, dest);
moved++;
}
else if (isDir)
{
if (MergeTree(entry, dest, ct)) Directory.Delete(entry, true);
dupes++;
}
else
{
if (new FileInfo(entry).Length == new FileInfo(dest).Length) File.Delete(entry);
dupes++;
}
}
log($"Cache merge: {moved} entries moved, {dupes} duplicates removed.");
}
public static void CopyInto(string source, string shared, Action<string> log, CancellationToken ct)
{
Directory.CreateDirectory(shared);
var copied = 0;
foreach (var file in Directory.EnumerateFiles(source, "*", SearchOption.AllDirectories))
{
ct.ThrowIfCancellationRequested();
var target = Path.Combine(shared, Path.GetRelativePath(source, file));
if (File.Exists(target)) continue;
Directory.CreateDirectory(Path.GetDirectoryName(target)!);
File.Copy(file, target);
copied++;
}
log($"Cache copy: {copied} files added to the shared cache.");
}
static bool MergeTree(string src, string dst, CancellationToken ct)
{
var clean = true;
foreach (var file in Directory.EnumerateFiles(src, "*", SearchOption.AllDirectories).ToList())
{
ct.ThrowIfCancellationRequested();
var rel = Path.GetRelativePath(src, file);
var target = Path.Combine(dst, rel);
if (!File.Exists(target))
{
Directory.CreateDirectory(Path.GetDirectoryName(target)!);
File.Move(file, target);
}
else if (new FileInfo(file).Length == new FileInfo(target).Length) File.Delete(file);
else clean = false;
}
return clean;
}
static void CopyTree(string src, string dst, CancellationToken ct)
{
Directory.CreateDirectory(dst);
foreach (var file in Directory.EnumerateFiles(src, "*", SearchOption.AllDirectories))
{
ct.ThrowIfCancellationRequested();
var target = Path.Combine(dst, Path.GetRelativePath(src, file));
Directory.CreateDirectory(Path.GetDirectoryName(target)!);
File.Copy(file, target, true);
}
}
public static void CreateJunction(string link, string target)
{
var psi = new ProcessStartInfo("cmd.exe")
{
CreateNoWindow = true,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
};
psi.ArgumentList.Add("/c");
psi.ArgumentList.Add("mklink");
psi.ArgumentList.Add("/J");
psi.ArgumentList.Add(link);
psi.ArgumentList.Add(target);
using var p = Process.Start(psi)!;
var err = p.StandardError.ReadToEnd() + p.StandardOutput.ReadToEnd();
p.WaitForExit();
if (p.ExitCode != 0) throw new IOException($"mklink failed: {err.Trim()}");
}
public static void RemoveLinkIfJunction(string path)
{
var info = new DirectoryInfo(path);
if (info.Exists && info.Attributes.HasFlag(FileAttributes.ReparsePoint)) Directory.Delete(path, false);
}
public static long SizeOf(string path)
{
if (!Directory.Exists(path)) return 0;
long total = 0;
foreach (var f in new DirectoryInfo(path).EnumerateFiles("*", SearchOption.AllDirectories)) total += f.Length;
return total;
}
}