Skip to content

Commit

Permalink
(chocolatey#3194) Add command to purge cached queries
Browse files Browse the repository at this point in the history
The changes in this commit adds a new command to Chocolatey CLI
that allows the user to clear any cached queries that have been saved
on their system.
This will clear both system and user level caches when running as an
administrator, and user level caches when running in a non-elevated
context.

Additionally, the ability to only remove expired caches is added as well
as just listing how many items has been cached.
  • Loading branch information
AdmiringWorm committed Jun 9, 2023
1 parent 77d5814 commit 2694868
Show file tree
Hide file tree
Showing 3 changed files with 336 additions and 1 deletion.
1 change: 1 addition & 0 deletions src/chocolatey/chocolatey.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@
</Compile>
<Compile Include="AssemblyExtensions.cs" />
<Compile Include="infrastructure.app\attributes\MultiServiceAttribute.cs" />
<Compile Include="infrastructure.app\commands\ChocolateyCacheCommand.cs" />
<Compile Include="infrastructure.app\commands\ChocolateyListCommand.cs" />
<Compile Include="infrastructure.app\commands\ChocolateyTemplateCommand.cs" />
<Compile Include="infrastructure.app\commands\ChocolateyCommandBase.cs" />
Expand Down
318 changes: 318 additions & 0 deletions src/chocolatey/infrastructure.app/commands/ChocolateyCacheCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,318 @@
// Copyright © 2023 Chocolatey Software, Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
//
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

namespace chocolatey.infrastructure.app.commands
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using chocolatey.infrastructure.app.attributes;
using chocolatey.infrastructure.app.configuration;
using chocolatey.infrastructure.app.nuget;
using chocolatey.infrastructure.commandline;
using chocolatey.infrastructure.commands;
using chocolatey.infrastructure.filesystem;
using chocolatey.infrastructure.logging;

[CommandFor("cache", "Manipulate the http caches that are used to store information")]
public class ChocolateyCacheCommand : ICommand
{
private readonly IFileSystem _fileSystem;

public ChocolateyCacheCommand(IFileSystem fileSystem)
{
_fileSystem = fileSystem;
}

public virtual void ConfigureArgumentParser(OptionSet optionSet, ChocolateyConfiguration configuration)
{
optionSet
.Add("remove",
"Remove - Remove the HTTP Cache used for the current launch context.",
option => configuration.ClearCache = option != null)
.Add("expired",
"Expired Only - Only remove cached items that has expired.",
option => configuration.ClearExpiredCacheOnly = option != null);
}

public virtual void DryRun(ChocolateyConfiguration configuration)
{
if (configuration.ClearCache)
{
RemoveCachedItems(configuration);
}
else
{
ListCacheStatistic(configuration);
}
}

public virtual void HelpMessage(ChocolateyConfiguration configuration)
{
this.Log().Info(ChocolateyLoggers.Important, "Cache Command");
this.Log().Info(@"
Get the statistics of what Chocolatey has cached, or clear any cached
items in the current context.
This is helpful for when it is necessary for the user to figure out
when the correct version of a package is not installed, but instead a
different version is used.");

this.Log().Info(ChocolateyLoggers.Important, "Usage");
this.Log().Info(@"
choco cache [options/switches]
");

this.Log().Info(ChocolateyLoggers.Important, "Examples");
this.Log().Info(@"
choco cache
choco cache --remove
choco cache --remove --expired
NOTE: See scripting in the command reference (`choco -?`) for how to
write proper scripts and integrations.");

this.Log().Info(ChocolateyLoggers.Important, "Exit Codes");
this.Log().Log().Info(@"
Exit codes that normally result from running this command.
Normal:
- 0: operation was successful, no issues detected
- -1 or 1: an error has occurred
If you find other exit codes that we have not yet documented, please
file a ticket so we can document it at
https://github.com/chocolatey/choco/issues/new/choose.
");
}

public virtual bool MayRequireAdminAccess()
{
// We will support cleaning the user cache directory without cleaning the system directory.
// As such it can be run without admin access.
return false;
}

public virtual void ParseAdditionalArguments(IList<string> unparsedArguments, ChocolateyConfiguration configuration)
{
// No additional arguments are supported right now.
}

public virtual void Run(ChocolateyConfiguration config)
{
if (config.ClearCache)
{
RemoveCachedItems(config);
}
else
{
ListCacheStatistic(config);
}
}

public virtual void Validate(ChocolateyConfiguration configuration)
{
// Nothing to validate
}

protected virtual void ListCacheStatistic(ChocolateyConfiguration configuration)
{
var userCacheLocation = ApplicationParameters.HttpCacheUserLocation;
var systemCacheLocation = ApplicationParameters.HttpCacheLocation;

if (userCacheLocation != systemCacheLocation)
{
this.Log().Info(ChocolateyLoggers.Important, "System Level HTTP Cache");
ListCachedItems(configuration, systemCacheLocation);

this.Log().Info(string.Empty);
this.Log().Info(ChocolateyLoggers.Important, "User Level HTTP Cache");
ListCachedItems(configuration, userCacheLocation);
}
else
{
this.Log().Info(ChocolateyLoggers.Important, "User Level HTTP Cache");
ListCachedItems(configuration, userCacheLocation);
}
}

private void ListCachedItems(ChocolateyConfiguration configuration, string cacheLocation)
{
var cachedFiles = _fileSystem.GetFiles(cacheLocation, "*.dat", SearchOption.AllDirectories);
var cachedDirectories = _fileSystem.GetDirectories(cacheLocation);
var expirationTimer = GetCacheExpiration(configuration);

var expiredFiles = cachedFiles.Where(f => _fileSystem.GetFileModifiedDate(f) < expirationTimer);

this.Log().Info("We found {0} cached sources.", cachedDirectories.Count());
this.Log().Info("We found {0} cached items for all sources, where {1} items has expired.", cachedFiles.Count(), expiredFiles.Count());
}

protected virtual void RemoveCachedItems(ChocolateyConfiguration configuration)
{
var systemCacheLocation = ApplicationParameters.HttpCacheLocation;
var userCacheLocation = ApplicationParameters.HttpCacheUserLocation;

this.Log().Info(ChocolateyLoggers.Important, "Cache cleanup");

if (userCacheLocation != systemCacheLocation)
{
CleanCachedItemsInPath(configuration, systemCacheLocation);
CleanCachedItemsInPath(configuration, userCacheLocation);
}
else
{
CleanCachedItemsInPath(configuration, userCacheLocation);

this.Log().Info(string.Empty);
this.Log().Warn("To remove system level HTTP cache, run the same command as an adiministrator!");
}
}

protected void CleanCachedItemsInPath(ChocolateyConfiguration configuration, string cacheLocation)
{
if (configuration.Noop && configuration.ClearExpiredCacheOnly)
{
this.Log().Info("Would remove all files with the .dat extension older than 30 minutes in '{0}'", cacheLocation);
return;
}
else if (configuration.Noop)
{
this.Log().Info("Would remove all files with the .dat in '{0}'", cacheLocation);
return;
}

var expirationTimer = GetCacheExpiration(configuration);

var filesBeforeClean = _fileSystem.GetFiles(cacheLocation, "*.dat", SearchOption.AllDirectories);

if (configuration.ClearExpiredCacheOnly)
{
filesBeforeClean = filesBeforeClean.Where(f => _fileSystem.GetFileModifiedDate(f) < expirationTimer);
}

var beforeFilesCount = filesBeforeClean.Count();

if (beforeFilesCount == 0)
{
this.Log().Info("No cached items available to be removed in '{0}'.", cacheLocation);
return;
}

if (configuration.ClearExpiredCacheOnly)
{
// We need to remove each individual file when the user only request
// deleting expired items. This takes a bit longer.
foreach (var fileToRemove in filesBeforeClean)
{
_fileSystem.DeleteFile(fileToRemove);
}

foreach (var directoryToRemove in _fileSystem.GetDirectories(cacheLocation))
{
if (!_fileSystem.GetFiles(directoryToRemove, "*", SearchOption.AllDirectories).Any())
{
_fileSystem.DeleteDirectoryChecked(directoryToRemove, recursive: false, overrideAttributes: false, isSilent: true);
}
}
}
else
{
foreach (var directoryToRemove in _fileSystem.GetDirectories(cacheLocation))
{
_fileSystem.DeleteDirectoryChecked(directoryToRemove, recursive: true);
}
}

var filesAfterClean = _fileSystem.GetFiles(cacheLocation, "*.dat", SearchOption.AllDirectories);

if (configuration.ClearExpiredCacheOnly)
{
filesAfterClean = filesAfterClean.Where(f => _fileSystem.GetFileModifiedDate(f) < expirationTimer);

this.Log().Info("Removed {0} expired cached items in '{1}'", beforeFilesCount - filesAfterClean.Count(), cacheLocation);
}
else
{
this.Log().Info("Removed {0} cached items in '{1}'", beforeFilesCount - filesAfterClean.Count(), cacheLocation);
}
}

private static DateTime GetCacheExpiration(ChocolateyConfiguration configuration)
{
DateTime? expirationTimer;
var cacheContext = new ChocolateySourceCacheContext(configuration);

if (cacheContext.MaxAge.HasValue)
{
expirationTimer = cacheContext.MaxAge.Value.DateTime;
}
else
{
expirationTimer = DateTime.Now.Subtract(cacheContext.MaxAgeTimeSpan);
}

return expirationTimer.Value;
}

#region Obsoleted methods

[Obsolete("Will be removed in v3. Use ConfigureArgumentParser instead!")]
public void configure_argument_parser(OptionSet optionSet, ChocolateyConfiguration configuration)
{
ConfigureArgumentParser(optionSet, configuration);
}

[Obsolete("Will be removed in v3. Use ParseAdditionalArguments instead!")]
public void handle_additional_argument_parsing(IList<string> unparsedArguments, ChocolateyConfiguration configuration)
{
ParseAdditionalArguments(unparsedArguments, configuration);
}

[Obsolete("Will be removed in v3. Use Validate instead!")]
public void handle_validation(ChocolateyConfiguration configuration)
{
Validate(configuration);
}

[Obsolete("Will be removed in v3. Use HelpMessage instead!")]
public void help_message(ChocolateyConfiguration configuration)
{
HelpMessage(configuration);
}

[Obsolete("Will be removed in v3. Use MayRequireAdminAccess instead!")]
public bool may_require_admin_access()
{
return MayRequireAdminAccess();
}

[Obsolete("Will be removed in v3. Use DryRun instead!")]
public void noop(ChocolateyConfiguration configuration)
{
DryRun(configuration);
}

[Obsolete("Will be removed in v3. Use Run instead!")]
public void run(ChocolateyConfiguration config)
{
Run(config);
}

#endregion Obsoleted methods
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2017 - 2022 Chocolatey Software, Inc
// Copyright © 2017 - 2023 Chocolatey Software, Inc
// Copyright © 2011 - 2017 RealDimensions Software, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
Expand Down Expand Up @@ -250,6 +250,22 @@ private void AppendOutput(StringBuilder propertyValues, string append)
// configuration set variables
public string CacheLocation { get; set; }

/// <summary>
/// Gets or sets a value indicating whether we should clear the cache or not.
/// </summary>
/// <value>
/// <c>true</c> if user has requested to clear the cache; otherwise, <c>false</c>.
/// </value>
public bool ClearCache { get; set; }

/// <summary>
/// Gets or sets a value indicating whether only expired items in the cache should be removed.
/// </summary>
/// <value>
/// <c>true</c> if only expired cache items should be removed; otherwise, <c>false</c>.
/// </value>
public bool ClearExpiredCacheOnly { get; set; }

public int CommandExecutionTimeoutSeconds { get; set; }
public int WebRequestTimeoutSeconds { get; set; }
public string DefaultTemplateName { get; set; }
Expand Down

0 comments on commit 2694868

Please sign in to comment.