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

Add cache command for managing downloaded installers #119

Merged
merged 7 commits into from
Jul 22, 2021
Merged
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ The **Windows Package Manager Manifest Creator** is available for download from
| [Submit](doc/submit.md) | Command for submitting an existing PR |
| [Token](doc/token.md) | Command for managing cached GitHub personal access tokens |
| [Settings](doc/settings.md) | Command for editing the settings file configurations |
| [Cache](doc/cache.md) | Command for managing downloaded installers stored in cache
| [-?](doc/help.md) | Displays command line help |

Click on the individual commands to learn more.
Expand Down
17 changes: 17 additions & 0 deletions doc/cache.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# cache command (Winget-Create)

The **cache** command of the [Winget-Create](../README.md) tool is designed to help users manage their downloaded installers. Continued usage of the tool can result in many downloaded installers taking up unnecessary space on your machine. This command can help clean up some of the unneeded files.

## Usage

`WingetCreateCLI.exe cache [\<options>]`

## Arguments

The following arguments are available:

| <div style="width:100px">Argument</div> | Description |
| --------------------------------------- | ------------|
| **-c, --clean** | Deletes all downloaded installers in the cache folder |
| **-l, --list** | Lists out all the downloaded installers stored in cache |
| **-o, --open** | Opens the cache folder storing the downloaded installers |
186 changes: 92 additions & 94 deletions src/WingetCreateCLI/Commands/BaseCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ namespace Microsoft.WingetCreateCLI.Commands
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using CommandLine;
using Microsoft.WingetCreateCLI.Logging;
using Microsoft.WingetCreateCLI.Properties;
using Microsoft.WingetCreateCLI.Telemetry;
Expand Down Expand Up @@ -47,8 +46,7 @@ public abstract class BaseCommand
/// <summary>
/// Gets or sets the GitHub token used to submit a pull request on behalf of the user.
/// </summary>
[Option('t', "token", Required = false, HelpText = "GitHubToken_HelpText", ResourceType = typeof(Resources))]
public string GitHubToken { get; set; }
public virtual string GitHubToken { get; set; }
ryfu-msft marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// Gets or sets the winget repo owner to use.
Expand Down Expand Up @@ -80,97 +78,6 @@ public abstract class BaseCommand
/// </summary>
protected GitHub GitHubClient { get; private set; }

/// <summary>
/// Validates the GitHubToken provided on the command-line, or if not present, the cached token if one exists.
/// Attempts a simple operation against the target repo, and if that fails, then:
/// If token provided on command-line, errors out
/// If not, and cached token was present, then deletes token cache, and starts OAuth flow
/// Otherwise, sets the instance variable to hold the validated token.
/// If no token is present on command-line or in cache, starts the OAuth flow to retrieve one.
/// </summary>
/// <param name="cacheToken">Boolean to override default behavior and force caching of token.</param>
/// <returns>True if the token is now present and valid, false otherwise.</returns>
public async Task<bool> SetAndCheckGitHubToken(bool cacheToken = false)
{
string cachedToken = null;
bool hasPatToken = !string.IsNullOrEmpty(this.GitHubToken);
string token = this.GitHubToken;

if (!hasPatToken)
{
Logger.Trace("No token parameter, reading cached token");
token = cachedToken = GitHubOAuth.ReadTokenCache();

if (string.IsNullOrEmpty(token))
{
Logger.Trace("No cached token found.");
Logger.DebugLocalized(nameof(Resources.GitHubAccountMustBeLinked_Message));
Logger.DebugLocalized(nameof(Resources.ExecutionPaused_Message));
Console.WriteLine();
token = await GitHubOAuthLoginFlow();
if (string.IsNullOrEmpty(token))
{
// User must've cancelled OAuth flow, we can't proceed successfully
Logger.WarnLocalized(nameof(Resources.NoTokenResponse_Message));
return false;
}

Logger.DebugLocalized(nameof(Resources.ResumingCommandExecution_Message));
}
else
{
Logger.DebugLocalized(nameof(Resources.UsingTokenFromCache_Message));
}
}

this.GitHubClient = new GitHub(token, this.WingetRepoOwner, this.WingetRepo);

try
{
Logger.Trace("Checking repo access using OAuth token");
await this.GitHubClient.CheckAccess();
Logger.Trace("Access check was successful, proceeding");
this.GitHubToken = token;

// Only cache the token if it came from Oauth, instead of PAT parameter or cache
if (cacheToken || (!hasPatToken && token != cachedToken))
{
try
{
Logger.Trace("Writing token to cache");
GitHubOAuth.WriteTokenCache(token);
}
catch (Exception ex)
{
// Failing to cache the token shouldn't be fatal.
Logger.WarnLocalized(nameof(Resources.WritingCacheTokenFailed_Message), ex.Message);
}
}

return true;
}
catch (Exception e)
{
if (token == cachedToken)
{
// There's an issue with the cached token, so let's delete it and try again
Logger.WarnLocalized(nameof(Resources.InvalidCachedToken));
GitHubOAuth.DeleteTokenCache();
return await this.SetAndCheckGitHubToken();
}
else if (e is AuthorizationException)
{
Logger.ErrorLocalized(nameof(Resources.Error_Prefix), e.Message);
Logger.ErrorLocalized(nameof(Resources.InvalidTokenError_Message));
return false;
}
else
{
throw;
}
}
}

/// <summary>
/// Abstract method executing the command.
/// </summary>
Expand Down Expand Up @@ -405,6 +312,97 @@ protected static void DisplayManifestPreview(Manifests manifests)
Console.WriteLine(manifests.DefaultLocaleManifest.ToYaml());
}

/// <summary>
/// Validates the GitHubToken provided on the command-line, or if not present, the cached token if one exists.
/// Attempts a simple operation against the target repo, and if that fails, then:
/// If token provided on command-line, errors out
/// If not, and cached token was present, then deletes token cache, and starts OAuth flow
/// Otherwise, sets the instance variable to hold the validated token.
/// If no token is present on command-line or in cache, starts the OAuth flow to retrieve one.
/// </summary>
/// <param name="cacheToken">Boolean to override default behavior and force caching of token.</param>
/// <returns>True if the token is now present and valid, false otherwise.</returns>
protected async Task<bool> SetAndCheckGitHubToken(bool cacheToken = false)
{
string cachedToken = null;
bool hasPatToken = !string.IsNullOrEmpty(this.GitHubToken);
string token = this.GitHubToken;

if (!hasPatToken)
{
Logger.Trace("No token parameter, reading cached token");
token = cachedToken = GitHubOAuth.ReadTokenCache();

if (string.IsNullOrEmpty(token))
{
Logger.Trace("No cached token found.");
Logger.DebugLocalized(nameof(Resources.GitHubAccountMustBeLinked_Message));
Logger.DebugLocalized(nameof(Resources.ExecutionPaused_Message));
Console.WriteLine();
token = await GitHubOAuthLoginFlow();
if (string.IsNullOrEmpty(token))
{
// User must've cancelled OAuth flow, we can't proceed successfully
Logger.WarnLocalized(nameof(Resources.NoTokenResponse_Message));
return false;
}

Logger.DebugLocalized(nameof(Resources.ResumingCommandExecution_Message));
}
else
{
Logger.DebugLocalized(nameof(Resources.UsingTokenFromCache_Message));
}
}

this.GitHubClient = new GitHub(token, this.WingetRepoOwner, this.WingetRepo);

try
{
Logger.Trace("Checking repo access using OAuth token");
await this.GitHubClient.CheckAccess();
Logger.Trace("Access check was successful, proceeding");
this.GitHubToken = token;

// Only cache the token if it came from Oauth, instead of PAT parameter or cache
if (cacheToken || (!hasPatToken && token != cachedToken))
{
try
{
Logger.Trace("Writing token to cache");
GitHubOAuth.WriteTokenCache(token);
}
catch (Exception ex)
{
// Failing to cache the token shouldn't be fatal.
Logger.WarnLocalized(nameof(Resources.WritingCacheTokenFailed_Message), ex.Message);
}
}

return true;
}
catch (Exception e)
{
if (token == cachedToken)
{
// There's an issue with the cached token, so let's delete it and try again
Logger.WarnLocalized(nameof(Resources.InvalidCachedToken));
GitHubOAuth.DeleteTokenCache();
return await this.SetAndCheckGitHubToken();
}
else if (e is AuthorizationException)
{
Logger.ErrorLocalized(nameof(Resources.Error_Prefix), e.Message);
Logger.ErrorLocalized(nameof(Resources.InvalidTokenError_Message));
return false;
}
else
{
throw;
}
}
}

/// <summary>
/// Submits a pull request with multifile manifests using the user's GitHub access token.
/// </summary>
Expand Down
103 changes: 103 additions & 0 deletions src/WingetCreateCLI/Commands/CacheCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.

namespace Microsoft.WingetCreateCLI.Commands
{
using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using CommandLine;
using Microsoft.WingetCreateCLI.Logging;
using Microsoft.WingetCreateCLI.Properties;
using Microsoft.WingetCreateCLI.Telemetry;
using Microsoft.WingetCreateCLI.Telemetry.Events;
using Microsoft.WingetCreateCore;

/// <summary>
/// Command to manage downloaded installers found in the %TEMP%/wingetcreate folder.
/// </summary>
[Verb("cache", HelpText = "CacheCommand_HelpText", ResourceType = typeof(Resources))]
public class CacheCommand : BaseCommand
{
/// <summary>
/// Gets or sets a value indicating whether to delete all downloaded installers found in cached.
/// </summary>
[Option('c', "clean", SetName = nameof(Clean), Required = true, HelpText = "Clean_HelpText", ResourceType = typeof(Resources))]
ryfu-msft marked this conversation as resolved.
Show resolved Hide resolved
public bool Clean { get; set; }

/// <summary>
/// Gets or sets a value indicating whether to list all downloaded installers found in cache.
/// </summary>
[Option('l', "list", Required = true, SetName = nameof(List), HelpText = "List_HelpText", ResourceType = typeof(Resources))]
public bool List { get; set; }

/// <summary>
/// Gets or sets a value indicating whether to open the cache folder containing all the downloaded installers.
/// </summary>
[Option('o', "open", Required = true, SetName = nameof(Open), HelpText = "Open_HelpText", ResourceType = typeof(Resources))]
public bool Open { get; set; }

/// <summary>
/// Executes the cache command flow.
/// </summary>
/// <returns>Boolean representing success or fail of the command.</returns>
public override Task<bool> Execute()
{
CommandExecutedEvent commandEvent = new CommandExecutedEvent
{
Command = nameof(CacheCommand),
};

try
{
if (!Directory.Exists(PackageParser.InstallerDownloadPath))
{
Directory.CreateDirectory(PackageParser.InstallerDownloadPath);
}

if (this.Clean)
{
DirectoryInfo dir = new DirectoryInfo(PackageParser.InstallerDownloadPath);
var files = dir.GetFiles();
Logger.InfoLocalized(nameof(Resources.InstallersFound_Message), files.Length, PackageParser.InstallerDownloadPath);
Console.WriteLine();

foreach (FileInfo file in files)
{
Logger.WarnLocalized(nameof(Resources.DeletingInstaller_Message), file.Name);
file.Delete();
}

Console.WriteLine();
Logger.InfoLocalized(nameof(Resources.InstallerCacheCleaned_Message));
}
else if (this.List)
{
string[] files = Directory.GetFiles(PackageParser.InstallerDownloadPath);
Logger.InfoLocalized(nameof(Resources.InstallersFound_Message), files.Length, PackageParser.InstallerDownloadPath);
ryfu-msft marked this conversation as resolved.
Show resolved Hide resolved
Console.WriteLine();

foreach (string file in files)
{
Logger.Debug(Path.GetFileName(file));
}
}
else if (this.Open)
{
Process.Start(new ProcessStartInfo()
{
FileName = PackageParser.InstallerDownloadPath,
UseShellExecute = true,
});
}

return Task.FromResult(commandEvent.IsSuccessful = true);
}
finally
{
TelemetryManager.Log.WriteEvent(commandEvent);
}
}
}
}
6 changes: 6 additions & 0 deletions src/WingetCreateCLI/Commands/NewCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ public static IEnumerable<Example> Examples
/// </summary>
[Option('o', "out", Required = false, HelpText = "OutputDirectory_HelpText", ResourceType = typeof(Resources))]
public string OutputDir { get; set; }

/// <summary>
/// Gets or sets the GitHub token used to submit a pull request on behalf of the user.
/// </summary>
[Option('t', "token", Required = false, HelpText = "GitHubToken_HelpText", ResourceType = typeof(Resources))]
public override string GitHubToken { get => base.GitHubToken; set => base.GitHubToken = value; }

/// <summary>
/// Executes the new command flow.
Expand Down
6 changes: 6 additions & 0 deletions src/WingetCreateCLI/Commands/SubmitCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ public static IEnumerable<Example> Examples
[Value(0, MetaName = "Path", Required = true, HelpText = "Path_HelpText", ResourceType = typeof(Resources))]
public string Path { get; set; }

/// <summary>
/// Gets or sets the GitHub token used to submit a pull request on behalf of the user.
/// </summary>
[Option('t', "token", Required = false, HelpText = "GitHubToken_HelpText", ResourceType = typeof(Resources))]
public override string GitHubToken { get => base.GitHubToken; set => base.GitHubToken = value; }

/// <summary>
/// Gets or sets the unbound arguments that exist after the first positional parameter.
/// </summary>
Expand Down
6 changes: 6 additions & 0 deletions src/WingetCreateCLI/Commands/TokenCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ public class TokenCommand : BaseCommand
[Option('s', "store", Required = true, SetName = nameof(Store), HelpText = "Store_HelpText", ResourceType = typeof(Resources))]
public bool Store { get; set; }

/// <summary>
/// Gets or sets the GitHub token used to submit a pull request on behalf of the user.
/// </summary>
[Option('t', "token", Required = false, HelpText = "GitHubToken_HelpText", ResourceType = typeof(Resources))]
public override string GitHubToken { get => base.GitHubToken; set => base.GitHubToken = value; }

/// <summary>
/// Executes the token command flow.
/// </summary>
Expand Down
6 changes: 6 additions & 0 deletions src/WingetCreateCLI/Commands/UpdateCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ public static IEnumerable<Example> Examples
[Option('s', "submit", Required = false, HelpText = "SubmitToWinget_HelpText", ResourceType = typeof(Resources))]
public bool SubmitToGitHub { get; set; }

/// <summary>
/// Gets or sets the GitHub token used to submit a pull request on behalf of the user.
/// </summary>
[Option('t', "token", Required = false, HelpText = "GitHubToken_HelpText", ResourceType = typeof(Resources))]
public override string GitHubToken { get => base.GitHubToken; set => base.GitHubToken = value; }

/// <summary>
/// Gets or sets the new value(s) used to update the manifest installer elements.
/// </summary>
Expand Down
Loading