-
Notifications
You must be signed in to change notification settings - Fork 3
NuGet Cleaner V1 #2
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| | ||
| Microsoft Visual Studio Solution File, Format Version 12.00 | ||
| # Visual Studio Version 16 | ||
| VisualStudioVersion = 16.0.29230.47 | ||
| MinimumVisualStudioVersion = 10.0.40219.1 | ||
| Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NuGetCleaner", "NuGetCleaner\NuGetCleaner.csproj", "{7A5AC6CF-FC9B-4904-811F-20DA4CF2661E}" | ||
| EndProject | ||
| Global | ||
| GlobalSection(SolutionConfigurationPlatforms) = preSolution | ||
| Debug|Any CPU = Debug|Any CPU | ||
| Release|Any CPU = Release|Any CPU | ||
| EndGlobalSection | ||
| GlobalSection(ProjectConfigurationPlatforms) = postSolution | ||
| {7A5AC6CF-FC9B-4904-811F-20DA4CF2661E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||
| {7A5AC6CF-FC9B-4904-811F-20DA4CF2661E}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||
| {7A5AC6CF-FC9B-4904-811F-20DA4CF2661E}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||
| {7A5AC6CF-FC9B-4904-811F-20DA4CF2661E}.Release|Any CPU.Build.0 = Release|Any CPU | ||
| EndGlobalSection | ||
| GlobalSection(SolutionProperties) = preSolution | ||
| HideSolutionNode = FALSE | ||
| EndGlobalSection | ||
| GlobalSection(ExtensibilityGlobals) = postSolution | ||
| SolutionGuid = {2E908518-7DF3-4925-8998-1332DA0C6B40} | ||
| EndGlobalSection | ||
| EndGlobal |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
|
|
||
| <PropertyGroup> | ||
| <OutputType>Exe</OutputType> | ||
| <TargetFramework>netcoreapp2.1</TargetFramework> | ||
|
|
||
| <PackAsTool>true</PackAsTool> | ||
| <ToolCommandName>NuGetCleaner</ToolCommandName> | ||
| <PackageOutputPath>./nupkg</PackageOutputPath> | ||
| <Authors>Christopher Gill</Authors> | ||
| <Company>Microsoft</Company> | ||
| <Description>Cleans up Global Package Folder by deleting directories unaccessed for a time greater than a specified threshold.</Description> | ||
| <PackageProjectUrl>https://github.com/chgill-MSFT/NuGetCleaner</PackageProjectUrl> | ||
| <RepositoryUrl>https://github.com/chgill-MSFT/NuGetCleaner</RepositoryUrl> | ||
| <GeneratePackageOnBuild>true</GeneratePackageOnBuild> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The .nupkg and .snupkg files should probably not be checked in to the repo. |
||
| <Version>1.0.8</Version> | ||
|
|
||
| <IncludeSymbols>true</IncludeSymbols> | ||
| <SymbolPackageFormat>snupkg</SymbolPackageFormat> | ||
| <SignAssembly>false</SignAssembly> | ||
|
|
||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <PackageReference Include="CommandLineParser" Version="2.6.0" /> | ||
| <PackageReference Include="NuGet.Configuration" Version="5.2.0" /> | ||
| </ItemGroup> | ||
|
|
||
| </Project> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using CommandLine; | ||
|
|
||
| namespace NuGetCleaner | ||
| { | ||
| // ReSharper disable once ClassNeverInstantiated.Global | ||
| public class Options | ||
| { | ||
| [Option("dry-run", HelpText = "Show packages that would be deleted without actually deleting them.")] | ||
| public bool DryRun { get; set; } | ||
|
|
||
| [Option("days", Required = true, HelpText = "Specify cut off for number of days since last package access. \n" + | ||
| "Packages last used in the number of days specified or greater will be deleted.")] | ||
| public int Days { get; set; } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,166 @@ | ||
| using System; | ||
| using System.Reflection; | ||
| using System.IO; | ||
| using CommandLine; | ||
| using System.ComponentModel; | ||
| using NuGet.Configuration; | ||
|
|
||
| namespace NuGetCleaner | ||
| { | ||
| class Program | ||
| { | ||
|
|
||
| public static void Main(string[] args) | ||
| { | ||
| var osNameAndVersion = System.Runtime.InteropServices.RuntimeInformation.OSDescription; | ||
| if (!osNameAndVersion.Contains("Windows")) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You should be able to do a more concrete "is windows" check with this: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider: private static IsWindows(){
if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows))
{
return true;
}
return false;
} |
||
| { | ||
| Console.WriteLine("This tool is currently only available for Windows, sorry for the inconvenience!\n"); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Explicit line endings are a bit strange. I would recommend either |
||
| return; | ||
| } | ||
|
|
||
| Parser.Default.ParseArguments<Options>(args) | ||
| .WithParsed(options => Execute(options)); | ||
| } | ||
|
|
||
| public static void Execute(Options options) | ||
| { | ||
| if (CheckDisableLastAccess() != 2) | ||
| { | ||
| Console.Write("\nYour Last Access updates are not currently enabled so this tool will not work. \n" + | ||
| "To enable Last Access updates, run powershell as administrator and input:\n\n" + | ||
| "\tfsutil behavior set disablelastaccess 2\n\n" + | ||
| "You may be asked to reboot for the settings change to take effect\n\n" + | ||
| "Note: Last usage time of packages will only be tracked once the setting is enabled.\n" + | ||
| "As such, last package usage will only be determined from the enable date onward.\n" + | ||
| "For further information view documentation at https://github.com/chgill-MSFT/NuGetCleaner \n"); | ||
| return; | ||
| } | ||
|
|
||
| var settings = Settings.LoadDefaultSettings("."); | ||
| var gpfPath = SettingsUtility.GetGlobalPackagesFolder(settings); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
|
|
||
| int Days = options.Days; | ||
|
|
||
| if (options.DryRun) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| { | ||
| SearchAndPrint(gpfPath, Days); | ||
| } | ||
| else { | ||
| SearchAndDestroy(gpfPath, Days); | ||
| } | ||
| } | ||
|
|
||
| public static int CheckDisableLastAccess() | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This method will only work on Windows, making the entire tool Windows only. If that's the intent, ok, but nothing in the package description makes that obvious, only if they look at the readme they might guess. So I can imagine people on Mac or Linux trying to install and use the tool. You could work cross platform if you did something similar to the following:
|
||
| { | ||
| System.Diagnostics.Process process = new System.Diagnostics.Process(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. dispose |
||
| System.Diagnostics.ProcessStartInfo startInfo = new System.Diagnostics.ProcessStartInfo(); | ||
| startInfo.UseShellExecute = false; | ||
| startInfo.RedirectStandardOutput = true; | ||
| startInfo.FileName = "CMD.exe"; | ||
| startInfo.Arguments = "/c fsutil behavior query disablelastaccess"; | ||
| process.StartInfo = startInfo; | ||
| process.Start(); | ||
| string output = process.StandardOutput.ReadToEnd(); | ||
| process.WaitForExit(); | ||
|
|
||
| string[] outputArray = output.Split(" "); | ||
| int setting = Convert.ToInt32(outputArray[2]); | ||
|
|
||
| return setting; | ||
| } | ||
|
|
||
| public static void SearchAndDestroy(string Path, int Days) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice method name 😎 |
||
| { | ||
| try | ||
| { | ||
| Console.WriteLine("Deleted Packages: "); | ||
|
|
||
| foreach (string pkg in Directory.GetDirectories(Path)) | ||
| { | ||
| foreach (string pkgVersion in Directory.GetDirectories(pkg)) | ||
| { | ||
| var dirAge = DateTime.Now - RecursiveFindLAT(pkgVersion, DateTime.MinValue); | ||
|
|
||
| if (dirAge.TotalDays >= Days) | ||
| { | ||
| Console.WriteLine(pkgVersion); | ||
| RecursiveDelete(pkgVersion); | ||
|
|
||
| if (Directory.GetDirectories(pkg).Length == 0) | ||
| { | ||
| Directory.Delete(pkg); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| catch (System.Exception ex) | ||
| { | ||
| Console.WriteLine(ex.Message); | ||
| } | ||
| } | ||
|
|
||
| public static void SearchAndPrint(string Path, int Days) | ||
| { | ||
| try | ||
| { | ||
| Console.WriteLine("DRY RUN (NOTHING IS DELETED)"); | ||
|
|
||
| Console.WriteLine("Would-Be Deleted Packages: \n"); | ||
|
|
||
| foreach (string pkg in Directory.GetDirectories(Path)) | ||
| { | ||
| foreach (string pkgVersion in Directory.GetDirectories(pkg)) | ||
| { | ||
|
|
||
| var dirAge = DateTime.Now - RecursiveFindLAT(pkgVersion, DateTime.MinValue); | ||
|
|
||
| if (dirAge.TotalDays >= Days) | ||
| { | ||
| Console.WriteLine(pkgVersion.Substring(Path.Length + 1) + " --- Last Access: " + RecursiveFindLAT(pkgVersion, DateTime.MinValue)); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| catch (System.Exception ex) | ||
| { | ||
| Console.WriteLine(ex.Message); | ||
| } | ||
| } | ||
|
|
||
| public static void RecursiveDelete(string dir) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| { | ||
| foreach (string subdir in Directory.GetDirectories(dir)) | ||
| { | ||
| RecursiveDelete(subdir); | ||
| } | ||
| foreach (string subdir in Directory.GetDirectories(dir)) | ||
| { | ||
| Directory.Delete(subdir); | ||
| } | ||
| foreach (string f in Directory.GetFiles(dir)) | ||
| { | ||
| File.Delete(f); | ||
| } | ||
| Directory.Delete(dir); | ||
| } | ||
|
|
||
| public static DateTime RecursiveFindLAT(string dir, DateTime dt) | ||
| { | ||
| foreach (string subdir in Directory.GetDirectories(dir)) | ||
| { | ||
| dt = RecursiveFindLAT(subdir, dt); | ||
| } | ||
| foreach (string f in Directory.GetFiles(dir)) | ||
| { | ||
| if (dt < File.GetLastAccessTime(f)) | ||
| { | ||
| dt = File.GetLastAccessTime(f); | ||
| } | ||
| } | ||
|
|
||
| return dt; | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| # NuGetCleaner | ||
|
|
||
| ## Description | ||
|
|
||
| The Global Packages Folder is where all packages installed through the NuGet package manager tool on Visual Studios are stored. When packages are no longer in use becuase a user has updated their packages for a project or has moved on to other projects, those unused packages remain in the GPF and continue to take up storage. It is not uncommon for the folder to have several Gigabytes of unnecessary package data after long-term use. | ||
|
|
||
| `nugetcleaner` is the solution! It is a global .NET CLI tool that deletes packages that haven't been used to build a project in a number of days specified by the user. | ||
|
|
||
| ## Installation | ||
|
|
||
| `nugetcleaner` is available as a .NET Core Global Tool: | ||
|
|
||
| ```bash | ||
| dotnet tool install --global NuGetCleaner --version 1.0.7 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
| ``` | ||
| The latest version can also be downloaded directly from NuGet.org at: | ||
| https://www.nuget.org/packages/NuGetCleaner/ | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| `nugetcleaner` makes use of last access timestamp updates which are disabled by default in Windows 7, 8, and 10. In order to use the tool, last access updates must be enabled. | ||
|
|
||
| In order to enable last access updates, run Windows PowerShell or Command Prompt in administrator mode and enter: | ||
| ```bash | ||
| fsutil behavior set disablelastaccess 2 | ||
| ``` | ||
|
|
||
| You may be prompted to reboot afterwards for the settings change to take effect. | ||
|
|
||
| It should be noted that last access timestamps will only begin to update after the settings change. Therefore, the package last access date will only be tracked from the enable date onward. | ||
|
|
||
| Last access updates were disabled by default starting in Windows Vista in order to increase performance. However, there seems to be little evidence pointing to noticeable performance decreases in more recent Windows OS version when enabling last access updates. However, if you would like to disable last access updates (revert to default) following successfull use of `nugetcleaner`, then run Windows PowerShell or Command Prompt in administrator mode and enter: | ||
| ```bash | ||
| fsutil behavior set disablelastaccess 3 | ||
| ``` | ||
|
|
||
| Again, you may be prompted to reboot afterwards for the settings change to take effect. | ||
|
|
||
| ## How It Works | ||
|
|
||
| `nugetcleaner` works by checking the last access date of all the packages in the global package folder and deleting ones with last access timestamps that exceed the age in days specified by the user. To be clear, it will check each version of a package and delete only the versions that exceed the age. For example, if there are multiple versions in the "newtonsoft.json" folder and only the most recent version has been used within the time constraint, then only the older version will be deleted. The "newtonsoft.json" parent directory will remain with the lastest version. However, if none of the versions were used within the time constraint, then all versions along with the parent folder will be deleted. At the end of the cleaning process, no empty folders will remain. | ||
|
|
||
| #### Use Warning: | ||
| If the cleaning process is activated immediately after last access updates enabled, before ongoing projects have been updated or built (thus accessing the relevant packages), then deletion will be determined solely by package installation date as last access timestamps wouldn't have had time to update. If this is an undesirable outcome, then it is highly recommended that the user waits until ongoing/relevant projects have been updated or built with last access timestamps enabled before using `nugetcleaner` to clean out the global package folder. If you're interested in seeing the potential outcome of the cleaning process before going through with it, enable `--dry-run` to see a preview of the packages that would be deleted. | ||
|
|
||
| ## Usage | ||
|
|
||
| ```bash | ||
| nugetcleaner [--dry-run] --days <# of days> | ||
| ``` | ||
|
|
||
| `--dry-run` - (Optional) List packages that would be deleted given the specified number of days, but don't actually delete them. | ||
|
|
||
| `--days <# of days>` - (Required) Packages last accessed (used in a project build) on the number of days in the past specified or greater are selected for deletion. | ||
|
|
||
| #### Example 1: Delete packages that have not been accessed in 60 or more days. | ||
|
|
||
| ```bash | ||
| nugetcleaner --days 60 | ||
| ``` | ||
|
|
||
| #### Example 2: List packages that have not been accessed in 90 or more days, but don't delete them. | ||
|
|
||
| ```bash | ||
| nugetcleaner --dry-run --days 90 | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| # Your spec's journey | ||
| * Create a new wiki page using the spec template below, to jot your thoughts down. | ||
| * Create an [issue](https://github.com/NuGet/Home/issues) to track the feature or link an existing issue to the new wiki page. Engage the community on the feature. Feel free to tweet it from the @NuGet handle or ask the PM to tweet it out. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This repo is not in the NuGet org, so I don't think it's appropriate to direct customers to file issues in NuGet/Home. |
||
| * Send a mail with a link to the wiki page to the core team alias. | ||
| * Campaign offline or in a meeting for the feature :. | ||
| * Once it is reviewed and signed off, a Manager or PM will move it to the Reviewed section. | ||
|
|
||
| ## Issue | ||
| TBD | ||
|
|
||
| ## Problem | ||
| There is currently no automated process to delete .npkg files from the Global Package Folder that are no longer needed/relevant. This presents a storage ineffiency that can be potentially significant for customers with very large or very many packages that are no longer in use. Customers who experience this problem currently must find the GPS folder themselves and delete the unwanted files manually. | ||
| ## Who is the customer? | ||
| Any .NET developer that installs packages (i.e. every .NET developer). | ||
|
|
||
| ## Evidence | ||
| At the moment, my evidence is that Karan says he has seen complaints from customers about this issue. Exact evidence TBD. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Who is Karan? :) maybe mention his alias There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Or better GitHub username |
||
|
|
||
| ## Solution | ||
|
|
||
| The soltuion will come in a form on a downloadable .NET CLI tool. The first version of this tool will be very simple and follow the steps oulined below: | ||
|
|
||
| 1. User invokes "clean" command | ||
| 2. GPS file metadata is checked for most recent read | ||
| 3. Files with read date > 30 days will be deleted | ||
| 4. List of deleted files will be printed to console | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should multitarget 3.0