Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update to .NET 5.0, add option for pruning #17

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,7 @@ comment][code-origin].
dry-run and report the clean-up that would be
done.
-m, --min-days=VALUE Number of days a package must not be used in order
to be purged from the cache. Defaults to 30.
to be purged from the cache. Defaults to 90.
-p, --prune prune older versions of packages regardless of age
-?, -h, --help show this message and exit

22 changes: 22 additions & 0 deletions dotnet-nuget-gc.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dotnet-nuget-gc", "src\dotnet-nuget-gc.csproj", "{A9AB4F95-C775-462A-8A49-62E3E701A119}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Documentation", "Documentation", "{D4DADB7C-5D3B-4C0A-8874-B6FE59008936}"
ProjectSection(SolutionItems) = preProject
LICENSE = LICENSE
README.md = README.md
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A9AB4F95-C775-462A-8A49-62E3E701A119}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A9AB4F95-C775-462A-8A49-62E3E701A119}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A9AB4F95-C775-462A-8A49-62E3E701A119}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A9AB4F95-C775-462A-8A49-62E3E701A119}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
2 changes: 2 additions & 0 deletions dotnet-nuget-gc.sln.DotSettings
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=prereleases/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
180 changes: 145 additions & 35 deletions src/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.IO;
using System.Linq;
using Mono.Options;
using NuGet.Versioning;

namespace NugetCacheCleaner
{
Expand All @@ -12,11 +13,20 @@ static void Main(string[] args)
{
var force = false;
var showHelp = false;
var minDays = TimeSpan.FromDays(30);
var minDays = TimeSpan.FromDays(90);
var prune = false;

var options = new OptionSet {
{"f|force", "Performs the actual clean-up. Default is to do a dry-run and report the clean-up that would be done.", v => force = v != null},
{"m|min-days=", "Number of days a package must not be used in order to be purged from the cache. Defaults to 30.", v => minDays = ParseDays(v)},
var options = new OptionSet
{
{
"f|force", "Performs the actual clean-up. Default is to do a dry-run and report the clean-up that would be done.",
v => force = v is not null
},
{
"m|min-days=", "Number of days a package must not be used in order to be purged from the cache. Defaults to 90.",
v => minDays = ParseDays(v)
},
{ "p|prune", "Prune older versions of packages regardless of age.", v => prune = v is not null },
{ "?|h|help", "Show this message.", v => showHelp = v != null },
};

Expand All @@ -37,7 +47,7 @@ static void Main(string[] args)
return;
}

var totalDeleted = CleanCache(force, minDays);
var totalDeleted = CleanCache(force, minDays, prune);
var mbDeleted = (totalDeleted / 1024d / 1024d).ToString("N0");

if (force)
Expand All @@ -46,7 +56,10 @@ static void Main(string[] args)
}
else
{
Console.WriteLine($"{mbDeleted} MB worth of packages are older than {minDays.TotalDays:N0} days.");
if (prune)
Console.WriteLine($"{mbDeleted} MB worth of packages are older than {minDays.TotalDays:N0} days or are not the latest version.");
else
Console.WriteLine($"{mbDeleted} MB worth of packages are older than {minDays.TotalDays:N0} days.");
Console.WriteLine("To delete, re-run with -f or --force flag.");
}
}
Expand All @@ -56,7 +69,7 @@ private static void ShowHelp(OptionSet optionSet)
Console.WriteLine("usage: dotnet nuget-gc [options]");
Console.WriteLine();
Console.WriteLine("Options:");
optionSet.WriteOptionDescriptions (Console.Out);
optionSet.WriteOptionDescriptions(Console.Out);
}

private static TimeSpan ParseDays(string text)
Expand All @@ -67,57 +80,154 @@ private static TimeSpan ParseDays(string text)
return TimeSpan.FromDays(days);
}

private static long CleanCache(bool force, TimeSpan minDays)
private static long CleanCache(bool force, TimeSpan minDays, bool prune)
{
var userProfilePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
var nugetCachePath = Path.Join(userProfilePath, ".nuget", "packages");
var nugetCache = new DirectoryInfo(nugetCachePath);
var totalDeleted = 0L;

var deleted = new HashSet<DirectoryInfo>();

void CleanPackageDirectory(DirectoryInfo dir)
{
DirectoryInfo versionDir;
FileInfo[] versionDirFiles;

long DeleteVersion(bool withLockCheck = false)
{
try
{
var size = versionDirFiles.Sum(f => f.Length);
DeleteDir(versionDir, force, withLockCheck);
deleted.Add(versionDir);
totalDeleted += size;
}
catch (FileNotFoundException)
{
// ok
}
catch (UnauthorizedAccessException)
{
Console.WriteLine($"Warning: Not authorized to delete {versionDir.FullName}.");
}
catch (Exception ex)
{
Console.WriteLine($"Warning: Deleting {versionDir.FullName} encountered {ex.GetType().Name}: {ex.Message}");
}
return totalDeleted;
}

var versions = new Dictionary<NuGetVersion, DirectoryInfo>();
deleted.Clear();
foreach (var subDir in dir.GetDirectories())
{
if (!NuGetVersion.TryParse(subDir.Name, out var version))
{
//Delete(versionFolder, force, withLockCheck: false);
Console.WriteLine($"Warning: Skipping non-version format directory {subDir.FullName}.");
continue;
}
versions.Add(version, subDir);
}

if (prune)
{
// keep newest release and newest prerelease (if newer than newest release)
var releases = versions.Keys.Where(v => !v.IsPrerelease).ToArray();
var newestRelease = releases.Any() ? releases.Max() : null;
var prereleases = versions.Keys.Where(v => v > newestRelease && v.IsPrerelease).ToArray();
var newestPrerelease = prereleases.Any() ? prereleases.Max() : null;

foreach (var versionedDir in versions)
{
if (versionedDir.Key == newestRelease) continue;
if (versionedDir.Key == newestPrerelease) continue;
versionDir = versionedDir.Value;
if (deleted.Contains(versionDir)) continue;
versionDirFiles = versionDir.GetFiles("*.*", SearchOption.AllDirectories);
totalDeleted += DeleteVersion(false);
}

foreach (var deletedDir in deleted)
{
var parsedVersion = versions.First(k => k.Value == deletedDir).Key;
versions.Remove(parsedVersion);
}
}

foreach (var versionedDir in versions)
{
versionDir = versionedDir.Value;
versionDirFiles = versionDir.GetFiles("*.*", SearchOption.AllDirectories);
if (versionDirFiles.Length == 0)
{
DeleteDir(versionDir, force, withLockCheck: false);
continue;
}

var lastAccessed = DateTime.UtcNow - versionDirFiles.Max(GetLastAccessed);

if (lastAccessed <= minDays)
continue;

Console.WriteLine($"{versionDir.FullName} last accessed {Math.Floor(lastAccessed.TotalDays)} days ago");

totalDeleted = DeleteVersion(true);
}
if (dir.GetDirectories().Length == 0)
DeleteDir(dir, force, withLockCheck: false);
}

if (!nugetCache.Exists)
{
Console.WriteLine($"Warning: Missing nuget package folder: {nugetCache.FullName}");
}
else
{
foreach (var folder in nugetCache.GetDirectories())
foreach (var dir in nugetCache.GetDirectories())
{
foreach (var versionFolder in folder.GetDirectories())
{
var files = versionFolder.GetFiles("*.*", SearchOption.AllDirectories);
if (files.Length == 0)
{
Delete(versionFolder, force, withLockCheck: false);
continue;
}
var size = files.Sum(f => f.Length);
var lastAccessed = DateTime.Now - files.Max(f => f.LastAccessTime);
if (lastAccessed > minDays)
{
Console.WriteLine($"{versionFolder.FullName} last accessed {Math.Floor(lastAccessed.TotalDays)} days ago");
try
{
Delete(versionFolder, force, withLockCheck: true);
totalDeleted += size;
}
catch { }
}
}
if (folder.GetDirectories().Length == 0)
Delete(folder, force, withLockCheck: false);
if (dir.Name != ".tools")
CleanPackageDirectory(dir);
else
foreach (var toolDir in dir.GetDirectories())
CleanPackageDirectory(toolDir);
}
}

return totalDeleted;
}

private static void Delete(DirectoryInfo dir, bool force, bool withLockCheck)
private static DateTime GetLastAccessed(FileInfo f)
{
try
{
return DateTime.FromFileTimeUtc(Math.Max(f.LastAccessTimeUtc.ToFileTimeUtc(), f.LastWriteTimeUtc.ToFileTimeUtc()));
}
catch
{
return f.LastWriteTimeUtc;
}
}

private static void DeleteDir(DirectoryInfo dir, bool force, bool withLockCheck)
{
if (!force)
{
#if DEBUG
Console.WriteLine($"Would remove {dir.FullName}.");
#endif
return;
}
#if DEBUG
Console.WriteLine($"Removing {dir.FullName}.");
#endif

if (withLockCheck) // This may only be good enough for Windows
{
var tempPath = Path.Join(dir.Parent.FullName, "_" + dir.Name);
var parentDir = dir.Parent;
if (parentDir == null) throw new NotImplementedException("Missing parent directory.");
var tempPath = Path.Join(parentDir.FullName, "_" + dir.Name);
dir.MoveTo(tempPath); // Attempt to rename before deleting
Directory.Delete(tempPath, recursive: true);
}
Expand All @@ -127,4 +237,4 @@ private static void Delete(DirectoryInfo dir, bool force, bool withLockCheck)
}
}
}
}
}
7 changes: 4 additions & 3 deletions src/dotnet-nuget-gc.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
<PackAsTool>true</PackAsTool>
<PackageOutputPath>$(OutDir)</PackageOutputPath>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
Expand All @@ -15,12 +15,13 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Mono.Options" Version="5.3.0.1" />
<PackageReference Include="Mono.Options" Version="6.6.0.161" />
<PackageReference Include="Nerdbank.GitVersioning">
<Version>2.2.13</Version>
<Version>3.4.216</Version>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="NuGet.Versioning" Version="5.10.0" />
</ItemGroup>

</Project>
2 changes: 1 addition & 1 deletion src/version.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "0.1",
"version": "0.2",
"publicReleaseRefSpec": [
"^refs/heads/master$" // we release out of master
]
Expand Down