9bb4f85802
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
170 lines
6.3 KiB
C#
170 lines
6.3 KiB
C#
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;
|
|
}
|
|
}
|