Skip to content

Commit

Permalink
Auto-delete @thumbs directories created by KSP1 at uninstall
Browse files Browse the repository at this point in the history
  • Loading branch information
HebaruSan committed Aug 23, 2023
1 parent ac4bac5 commit c04a077
Show file tree
Hide file tree
Showing 6 changed files with 221 additions and 19 deletions.
8 changes: 4 additions & 4 deletions Core/Games/IGame.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ public interface IGame
// What do we contain?
string PrimaryModDirectoryRelative { get; }
string PrimaryModDirectory(GameInstance inst);
string[] StockFolders { get; }
string[] ReservedPaths { get; }
string[] CreateableDirs { get; }
string[] StockFolders { get; }
string[] ReservedPaths { get; }
string[] CreateableDirs { get; }
string[] AutoRemovableDirs { get; }
bool IsReservedDirectory(GameInstance inst, string path);
bool AllowInstallationIn(string name, out string path);
void RebuildSubdirectories(string absGameRoot);
Expand All @@ -39,5 +40,4 @@ public interface IGame
Uri DefaultRepositoryURL { get; }
Uri RepositoryListURL { get; }
}

}
5 changes: 5 additions & 0 deletions Core/Games/KerbalSpaceProgram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,11 @@ public string PrimaryModDirectory(GameInstance inst)
"GameData", "Tutorial", "Scenarios", "Missions", "Ships/Script"
};

public string[] AutoRemovableDirs => new string[]
{
"@thumbs"
};

/// <summary>
/// Checks the path against a list of reserved game directories
/// </summary>
Expand Down
2 changes: 2 additions & 0 deletions Core/Games/KerbalSpaceProgram2.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ public string PrimaryModDirectory(GameInstance inst)
"BepInEx/plugins",
};

public string[] AutoRemovableDirs => new string[] { };

/// <summary>
/// Checks the path against a list of reserved game directories
/// </summary>
Expand Down
81 changes: 69 additions & 12 deletions Core/ModuleInstaller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using CKAN.Extensions;
using CKAN.Versioning;
using CKAN.Configuration;
using CKAN.Games;

namespace CKAN
{
Expand Down Expand Up @@ -762,7 +763,7 @@ private void Uninstall(string modName, ref HashSet<string> possibleConfigOnlyDir
}

// Walk our registry to find all files for this mod.
IEnumerable<string> files = mod.Files;
var files = mod.Files.ToArray();

// We need case insensitive path matching on Windows
var directoriesToDelete = Platform.IsWindows
Expand Down Expand Up @@ -826,7 +827,7 @@ private void Uninstall(string modName, ref HashSet<string> possibleConfigOnlyDir

// Sort our directories from longest to shortest, to make sure we remove child directories
// before parents. GH #78.
foreach (string directory in directoriesToDelete.OrderBy(dir => dir.Length).Reverse())
foreach (string directory in directoriesToDelete.OrderByDescending(dir => dir.Length))
{
log.DebugFormat("Checking {0}...", directory);
// It is bad if any of this directories gets removed
Expand All @@ -838,16 +839,42 @@ private void Uninstall(string modName, ref HashSet<string> possibleConfigOnlyDir
continue;
}

var contents = Directory
.EnumerateFileSystemEntries(directory, "*", SearchOption.AllDirectories)
.Select(f => ksp.ToRelativeGameDir(f))
.Memoize();
log.DebugFormat("Got contents: {0}", string.Join(", ", contents));
var owners = contents.Select(f => registry.FileOwner(f));
log.DebugFormat("Got owners: {0}", string.Join(", ", owners));
if (!contents.Any())
// See what's left in this folder and what we can do about it
GroupFilesByRemovable(ksp.ToRelativeGameDir(directory),
registry, files, ksp.game,
(Directory.Exists(directory)
? Directory.EnumerateFileSystemEntries(directory, "*", SearchOption.AllDirectories)
: Enumerable.Empty<string>())
.Select(f => ksp.ToRelativeGameDir(f)),
out string[] removable,
out string[] notRemovable);

// Delete the auto-removable files and dirs
foreach (var relPath in removable)
{
var absPath = ksp.ToAbsoluteGameDir(relPath);
if (File.Exists(absPath))
{
log.DebugFormat("Attempting transaction deletion of file {0}", absPath);
file_transaction.Delete(absPath);
}
else if (Directory.Exists(absPath))
{
log.DebugFormat("Attempting deletion of directory {0}", absPath);
try
{
Directory.Delete(absPath);
}
catch
{
// There might be files owned by other mods, oh well
log.DebugFormat("Failed to delete {0}", absPath);
}
}
}

if (!notRemovable.Any())
{
// We *don't* use our file_transaction to delete files here, because
// it fails if the system's temp directory is on a different device
// to KSP. However we *can* safely delete it now we know it's empty,
Expand All @@ -860,9 +887,10 @@ private void Uninstall(string modName, ref HashSet<string> possibleConfigOnlyDir
log.DebugFormat("Removing {0}", directory);
Directory.Delete(directory);
}
else if (contents.All(f => registry.FileOwner(f) == null))
else if (notRemovable.All(f => registry.FileOwner(f) == null && !files.Contains(f)))
{
log.DebugFormat("Directory {0} contains only non-registered files, ask user about it later", directory);
log.DebugFormat("Directory {0} contains only non-registered files, ask user about it later: {1}",
directory, string.Join(", ", notRemovable));
if (possibleConfigOnlyDirs == null)
{
possibleConfigOnlyDirs = new HashSet<string>();
Expand All @@ -879,6 +907,35 @@ private void Uninstall(string modName, ref HashSet<string> possibleConfigOnlyDir
}
}

internal static void GroupFilesByRemovable(string relRoot,
Registry registry,
string[] alreadyRemoving,
IGame game,
IEnumerable<string> relPaths,
out string[] removable,
out string[] notRemovable)
{
log.DebugFormat("Getting contents of {0}", relRoot);
var contents = relPaths
// Split into auto-removable and not-removable
// Removable must not be owned by other mods
.GroupBy(f => registry.FileOwner(f) == null
// Also skip owned by this module since it's already deregistered
&& !alreadyRemoving.Contains(f)
// Must have a removable dir name somewhere in path AFTER main dir
&& f.Substring(relRoot.Length)
.Split('/')
.Where(piece => !string.IsNullOrEmpty(piece))
.Any(piece => game.AutoRemovableDirs.Contains(piece)))
.ToDictionary(grp => grp.Key,
grp => grp.OrderByDescending(f => f.Length)
.ToArray());
removable = contents.TryGetValue(true, out string[] val1) ? val1 : new string[] {};
notRemovable = contents.TryGetValue(false, out string[] val2) ? val2 : new string[] {};
log.DebugFormat("Got removable: {0}", string.Join(", ", removable));
log.DebugFormat("Got notRemovable: {0}", string.Join(", ", notRemovable));
}

/// <summary>
/// Takes a collection of directories and adds all parent directories within the GameData structure.
/// </summary>
Expand Down
6 changes: 4 additions & 2 deletions GUI/Main/MainInstall.cs
Original file line number Diff line number Diff line change
Expand Up @@ -268,8 +268,10 @@ private void HandlePossibleConfigOnlyDirs(Registry registry, HashSet<string> pos
// Check again for registered files, since we may
// just have installed or upgraded some
possibleConfigOnlyDirs.RemoveWhere(
d => Directory.EnumerateFileSystemEntries(d, "*", SearchOption.AllDirectories)
.Any(f => registry.FileOwner(CurrentInstance.ToRelativeGameDir(f)) != null));
d => !Directory.Exists(d)
|| Directory.EnumerateFileSystemEntries(d, "*", SearchOption.AllDirectories)
.Select(absF => CurrentInstance.ToRelativeGameDir(absF))
.Any(relF => registry.FileOwner(relF) != null));
if (possibleConfigOnlyDirs.Count > 0)
{
AddStatusMessage("");
Expand Down
138 changes: 137 additions & 1 deletion Tests/Core/ModuleInstaller.cs → Tests/Core/ModuleInstallerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Linq;
using System.Text.RegularExpressions;
using System.Transactions;

using ICSharpCode.SharpZipLib.Zip;
using NUnit.Framework;

Expand All @@ -16,7 +17,7 @@
namespace Tests.Core
{
[TestFixture]
public class ModuleInstaller
public class ModuleInstallerTests
{
private string flag_path;
private string dogezip;
Expand Down Expand Up @@ -607,6 +608,141 @@ public void UninstallEmptyDirs()
}
}

[Test,
// Empty dir
TestCase("GameData/SomeMod/Parts",
new string[] {},
new string[] {},
new string[] {},
new string[] {}),
// A few regular files and some thumbnails
TestCase("GameData/SomeMod/Parts",
new string[] {},
new string[]
{
"GameData/SomeMod/Parts/userfile.cfg",
"GameData/SomeMod/Parts/userfile2.cfg",
"GameData/SomeMod/Parts/@thumbs",
"GameData/SomeMod/Parts/@thumbs/part1.png",
"GameData/SomeMod/Parts/@thumbs/part3.png",
"GameData/SomeMod/Parts/@thumbs/part4.png",
},
new string[]
{
"GameData/SomeMod/Parts/@thumbs/part1.png",
"GameData/SomeMod/Parts/@thumbs/part3.png",
"GameData/SomeMod/Parts/@thumbs/part4.png",
"GameData/SomeMod/Parts/@thumbs",
},
new string[]
{
"GameData/SomeMod/Parts/userfile2.cfg",
"GameData/SomeMod/Parts/userfile.cfg",
}),
// Just regular files
TestCase("GameData/SomeMod/Parts",
new string[] {},
new string[]
{
"GameData/SomeMod/Parts/userfile.cfg",
"GameData/SomeMod/Parts/userfile2.cfg",
},
new string[] {},
new string[]
{
"GameData/SomeMod/Parts/userfile2.cfg",
"GameData/SomeMod/Parts/userfile.cfg",
}),
// Just thumbnails
TestCase("GameData/SomeMod/Parts",
new string[] {},
new string[]
{
"GameData/SomeMod/Parts/@thumbs",
"GameData/SomeMod/Parts/@thumbs/part1.png",
"GameData/SomeMod/Parts/@thumbs/part3.png",
"GameData/SomeMod/Parts/@thumbs/part4.png",
},
new string[]
{
"GameData/SomeMod/Parts/@thumbs/part1.png",
"GameData/SomeMod/Parts/@thumbs/part3.png",
"GameData/SomeMod/Parts/@thumbs/part4.png",
"GameData/SomeMod/Parts/@thumbs",
},
new string[] {}),
// A few regular files and some thumbnails, some of which are owned by another mod
TestCase("GameData/SomeMod/Parts",
new string[]
{
"GameData/SomeMod/Parts/userfile2.cfg",
"GameData/SomeMod/Parts/@thumbs/part1.png",
},
new string[]
{
"GameData/SomeMod/Parts/userfile.cfg",
"GameData/SomeMod/Parts/userfile2.cfg",
"GameData/SomeMod/Parts/@thumbs",
"GameData/SomeMod/Parts/@thumbs/part1.png",
"GameData/SomeMod/Parts/@thumbs/part3.png",
"GameData/SomeMod/Parts/@thumbs/part4.png",
},
new string[]
{
"GameData/SomeMod/Parts/@thumbs/part3.png",
"GameData/SomeMod/Parts/@thumbs/part4.png",
"GameData/SomeMod/Parts/@thumbs",
},
new string[]
{
"GameData/SomeMod/Parts/@thumbs/part1.png",
"GameData/SomeMod/Parts/userfile2.cfg",
"GameData/SomeMod/Parts/userfile.cfg",
}),
]
public void GroupFilesByRemovable_WithFiles_CorrectOutput(string relRoot,
string[] registeredFiles,
string[] relPaths,
string[] correctRemovable,
string[] correctNotRemovable)
{
// Arrange
using (var inst = new DisposableKSP())
{
var game = new KerbalSpaceProgram();
var registry = CKAN.RegistryManager.Instance(inst.KSP).registry;
// Make files to be registered to another mod
var absFiles = registeredFiles.Select(f => inst.KSP.ToAbsoluteGameDir(f))
.ToArray();
foreach (var absPath in absFiles)
{
Directory.CreateDirectory(Path.GetDirectoryName(absPath));
File.Create(absPath).Dispose();
}
// Register the other mod
registry.RegisterModule(CkanModule.FromJson(@"{
""spec_version"": 1,
""identifier"": ""otherMod"",
""version"": ""1.0"",
""download"": ""https://github.com/""
}"),
absFiles, inst.KSP, false);

// Act
CKAN.ModuleInstaller.GroupFilesByRemovable(relRoot,
registry,
new string[] {},
game,
relPaths,
out string[] removable,
out string[] notRemovable);

// Assert
Assert.AreEqual(correctRemovable, removable);
Assert.AreEqual(correctNotRemovable, notRemovable);
}
}

[Test]
public void ModuleManagerInstancesAreDecoupled()
{
Expand Down

0 comments on commit c04a077

Please sign in to comment.