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 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 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 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; } }