Skip to content

Commit 8a284f0

Browse files
authored
Merge pull request #2849 from Nexus-Mods/fix/clear-empty-folders
Cleanup empty directories when switching loadouts
2 parents 8156604 + 56f8bf5 commit 8a284f0

File tree

2 files changed

+49
-8
lines changed

2 files changed

+49
-8
lines changed

src/Abstractions/NexusMods.Abstractions.Loadouts.Synchronizers/ALoadoutSynchronizer.cs

+14-6
Original file line numberDiff line numberDiff line change
@@ -553,7 +553,7 @@ public async Task RunActions(Dictionary<GamePath, SyncNode> syncTree, GameInstal
553553
var gameMetadataId = gameInstallation.GameMetadataId;
554554
var gameMetadata = GameInstallMetadata.Load(Connection.Db, gameMetadataId);
555555
var register = gameInstallation.LocationsRegister;
556-
var deletedFiles = new HashSet<(GamePath GamePath, AbsolutePath FullPath)>();
556+
HashSet<(GamePath GamePath, AbsolutePath FullPath)> foldersWithDeletedFiles = [];
557557

558558
foreach (var action in ActionsInOrder)
559559
{
@@ -574,7 +574,7 @@ public async Task RunActions(Dictionary<GamePath, SyncNode> syncTree, GameInstal
574574
break;
575575

576576
case Actions.DeleteFromDisk:
577-
ActionDeleteFromDisk(syncTree, register, tx, gameInstallation.GameMetadataId, deletedFiles);
577+
ActionDeleteFromDisk(syncTree, register, tx, gameInstallation.GameMetadataId, foldersWithDeletedFiles);
578578
break;
579579

580580
case Actions.ExtractToDisk:
@@ -608,7 +608,15 @@ public async Task RunActions(Dictionary<GamePath, SyncNode> syncTree, GameInstal
608608
}
609609
tx.Add(gameMetadataId, GameInstallMetadata.LastScannedDiskStateTransaction, EntityId.From(tx.ThisTxId.Value));
610610

611-
await tx.Commit();
611+
var result = await tx.Commit();
612+
613+
var newMetadata = gameMetadata.Rebase(result.Db);
614+
615+
// Clean up empty directories
616+
if (foldersWithDeletedFiles.Count > 0)
617+
{
618+
CleanDirectories(foldersWithDeletedFiles, newMetadata.DiskStateEntries, gameInstallation);
619+
}
612620
}
613621

614622
private void WarnOfConflict(Dictionary<GamePath, SyncNode> tree)
@@ -757,12 +765,13 @@ private void ActionDeleteFromDisk(Dictionary<GamePath, SyncNode> groupings, IGam
757765
continue;
758766
var resolvedPath = register.GetResolvedPath(path);
759767
resolvedPath.Delete();
760-
foldersWithDeletedFiles.Add((path, resolvedPath.Parent));
761768

762769

763-
// Don't delete the entry if we're just going to replace it
770+
// Only delete the entry if we're not going to replace it
764771
if (!node.Actions.HasFlag(Actions.ExtractToDisk))
765772
{
773+
foldersWithDeletedFiles.Add((path, resolvedPath.Parent));
774+
766775
var id = node.Disk.EntityId;
767776
tx.Retract(id, DiskStateEntry.Path, ((EntityId)gameMetadataId, path.LocationId, path.Path));
768777
tx.Retract(id, DiskStateEntry.Hash, node.Disk.Hash);
@@ -828,7 +837,6 @@ private bool ActionIngestFromDisk(Dictionary<GamePath, SyncNode> syncTree, Loado
828837
var prevLoadout = Loadout.Load(loadout.Db, lastAppliedId);
829838
if (prevLoadout.IsValid())
830839
{
831-
await Synchronize(prevLoadout);
832840
await DeactivateCurrentLoadout(loadout.InstallationInstance);
833841
await ActivateLoadout(loadout);
834842
return loadout.Rebase();

tests/NexusMods.DataModel.Synchronizer.Tests/SynchronizerUnitTests.cs

+35-2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,33 @@ namespace NexusMods.DataModel.Synchronizer.Tests;
1616
/// </summary>
1717
public class SynchronizerUnitTests(ITestOutputHelper testOutputHelper) : ACyberpunkIsolatedGameTest<SynchronizerUnitTests>(testOutputHelper)
1818
{
19+
20+
[Fact]
21+
[GithubIssue(2077)]
22+
public async Task EmptyFoldersAreRemovedWhenSwitchingLoadouts()
23+
{
24+
var loadoutA = await CreateLoadout();
25+
26+
var nestedFile = new GamePath(LocationId.Game, "a/b/nested.txt");
27+
var nestedFileFullPath = GameInstallation.LocationsRegister.GetResolvedPath(nestedFile);
28+
29+
nestedFileFullPath.Parent.CreateDirectory();
30+
await nestedFileFullPath.WriteAllTextAsync("Nested File");
31+
32+
loadoutA = await Synchronizer.Synchronize(loadoutA);
33+
34+
loadoutA.Items.Should().ContainSingle(f => f.Name == "nested.txt");
35+
36+
// Create new empty loadout
37+
var loadoutB = await CreateLoadout();
38+
39+
// Switch to empty loadout
40+
loadoutB = await Synchronizer.Synchronize(loadoutB);
41+
42+
// 'a/' directory should be deleted
43+
nestedFileFullPath.Parent.Parent.DirectoryExists().Should().BeFalse();
44+
}
45+
1946
[Fact]
2047
[GithubIssue(1925)]
2148
public async Task EmptyChildFoldersDontDeleteNonEmptyParents()
@@ -38,7 +65,6 @@ public async Task EmptyChildFoldersDontDeleteNonEmptyParents()
3865

3966
loadout.Items.Should().ContainSingle(f => f.Name == "parent.txt");
4067
loadout.Items.Should().ContainSingle(f => f.Name == "grandchild.txt");
41-
4268

4369

4470
using (var tx = Connection.BeginTransaction())
@@ -50,8 +76,15 @@ public async Task EmptyChildFoldersDontDeleteNonEmptyParents()
5076

5177
loadout = loadout.Rebase();
5278
loadout = await Synchronizer.Synchronize(loadout);
53-
79+
80+
// a/b/c/grandchild.txt
5481
grandChildFileFullPath.FileExists.Should().BeFalse();
82+
// a/b/c
83+
grandChildFileFullPath.Parent.DirectoryExists().Should().BeFalse();
84+
// a/b
85+
grandChildFileFullPath.Parent.Parent.DirectoryExists().Should().BeFalse();
86+
87+
// a/parent.txt
5588
parentFileFullPath.FileExists.Should().BeTrue();
5689
}
5790

0 commit comments

Comments
 (0)