CVR Manager 1.0.0
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user