diff --git a/src/chocolatey.tests/chocolatey.tests.csproj b/src/chocolatey.tests/chocolatey.tests.csproj index 92d428eade..b3197b2dc3 100644 --- a/src/chocolatey.tests/chocolatey.tests.csproj +++ b/src/chocolatey.tests/chocolatey.tests.csproj @@ -88,6 +88,7 @@ + diff --git a/src/chocolatey.tests/infrastructure.app/commands/ChocolateyExportCommandSpecs.cs b/src/chocolatey.tests/infrastructure.app/commands/ChocolateyExportCommandSpecs.cs new file mode 100644 index 0000000000..d195c0c3f7 --- /dev/null +++ b/src/chocolatey.tests/infrastructure.app/commands/ChocolateyExportCommandSpecs.cs @@ -0,0 +1,220 @@ +// Copyright © 2017 - 2018 Chocolatey Software, Inc +// Copyright © 2011 - 2017 RealDimensions Software, LLC +// +// 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.tests.infrastructure.app.commands +{ + using System; + using System.Collections.Generic; + using System.Linq; + using chocolatey.infrastructure.app.attributes; + using chocolatey.infrastructure.app.commands; + using chocolatey.infrastructure.app.configuration; + using chocolatey.infrastructure.app.services; + using chocolatey.infrastructure.commandline; + using chocolatey.infrastructure.filesystem; + using Moq; + using Should; + + public class ChocolateyExportCommandSpecs + { + public abstract class ChocolateyExportCommandSpecsBase : TinySpec + { + protected ChocolateyExportCommand command; + protected Mock nugetService = new Mock(); + protected Mock fileSystem = new Mock(); + protected ChocolateyConfiguration configuration = new ChocolateyConfiguration(); + + public override void Context() + { + command = new ChocolateyExportCommand(nugetService.Object, fileSystem.Object); + } + + public void reset() + { + nugetService.ResetCalls(); + fileSystem.ResetCalls(); + } + } + + public class when_implementing_command_for : ChocolateyExportCommandSpecsBase + { + private List results; + + public override void Because() + { + results = command.GetType().GetCustomAttributes(typeof(CommandForAttribute), false).Cast().Select(a => a.CommandName).ToList(); + } + + [Fact] + public void should_implement_help() + { + results.ShouldContain("export"); + } + } + + public class when_configurating_the_argument_parser : ChocolateyExportCommandSpecsBase + { + private OptionSet optionSet; + + public override void Context() + { + base.Context(); + optionSet = new OptionSet(); + } + + public override void Because() + { + command.configure_argument_parser(optionSet, configuration); + } + + [Fact] + public void should_add_output_file_path_to_the_option_set() + { + optionSet.Contains("output-file-path").ShouldBeTrue(); + } + + [Fact] + public void should_add_short_version_of_output_file_path_to_the_option_set() + { + optionSet.Contains("o").ShouldBeTrue(); + } + + [Fact] + public void should_add_include_version_numbers_to_the_option_set() + { + optionSet.Contains("include-version-numbers").ShouldBeTrue(); + } + + [Fact] + public void should_add_include_version_to_the_option_set() + { + optionSet.Contains("include-version").ShouldBeTrue(); + } + } + + public class when_handling_additional_argument_parsing : ChocolateyExportCommandSpecsBase + { + private readonly IList unparsedArgs = new List(); + private Action because; + + public override void Because() + { + because = () => command.handle_additional_argument_parsing(unparsedArgs, configuration); + } + + public new void reset() + { + configuration.ExportCommand.OutputFilePath = string.Empty; + unparsedArgs.Clear(); + base.reset(); + } + + [Fact] + public void should_handle_passing_in_an_empty_string_for_output_file_path() + { + reset(); + unparsedArgs.Add(" "); + because(); + + configuration.ExportCommand.OutputFilePath.ShouldEqual("packages.config"); + } + + [Fact] + public void should_handle_passing_in_a_string_for_output_file_path() + { + reset(); + unparsedArgs.Add("custompackages.config"); + because(); + + configuration.ExportCommand.OutputFilePath.ShouldEqual("custompackages.config"); + } + } + + public class when_noop_is_called : ChocolateyExportCommandSpecsBase + { + public override void Because() + { + command.noop(configuration); + } + + [Fact] + public void should_log_a_message() + { + MockLogger.Verify(l => l.Info(It.IsAny()), Times.AtLeastOnce); + } + + [Fact] + public void should_log_the_message_we_expect() + { + var messages = MockLogger.MessagesFor(LogLevel.Info); + messages.ShouldNotBeEmpty(); + messages.Count.ShouldEqual(1); + messages[0].ShouldContain("Export would have been with options"); + } + } + + public class when_run_is_called : ChocolateyExportCommandSpecsBase + { + public new void reset() + { + Context(); + base.reset(); + } + + public override void AfterEachSpec() + { + base.AfterEachSpec(); + MockLogger.Messages.Clear(); + } + + public override void Because() + { + // because = () => command.run(configuration); + } + + [Fact] + public void should_call_nuget_service_get_all_installed_packages() + { + reset(); + command.run(configuration); + + nugetService.Verify(n => n.get_all_installed_packages(It.IsAny()), Times.Once); + } + + [Fact] + public void should_call_replace_file_when_file_already_exists() + { + fileSystem.Setup(f => f.file_exists(It.IsAny())).Returns(true); + + reset(); + command.run(configuration); + + fileSystem.Verify(n => n.replace_file(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public void should_not_call_replace_file_when_file_doesnt_exist() + { + fileSystem.Setup(f => f.file_exists(It.IsAny())).Returns(false); + + reset(); + command.run(configuration); + + fileSystem.Verify(n => n.replace_file(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } + } + } +} diff --git a/src/chocolatey/chocolatey.csproj b/src/chocolatey/chocolatey.csproj index 5c7a16224c..6635f7dd4e 100644 --- a/src/chocolatey/chocolatey.csproj +++ b/src/chocolatey/chocolatey.csproj @@ -113,6 +113,7 @@ Properties\SolutionVersion.cs + diff --git a/src/chocolatey/infrastructure.app/commands/ChocolateyExportCommand.cs b/src/chocolatey/infrastructure.app/commands/ChocolateyExportCommand.cs new file mode 100644 index 0000000000..177f68d221 --- /dev/null +++ b/src/chocolatey/infrastructure.app/commands/ChocolateyExportCommand.cs @@ -0,0 +1,197 @@ +// Copyright © 2017 - 2018 Chocolatey Software, Inc +// Copyright © 2011 - 2017 RealDimensions Software, LLC +// +// 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.Diagnostics; + using System.IO; + using System.Text; + using System.Xml; + using attributes; + using commandline; + using configuration; + using filesystem; + using infrastructure.commands; + using logging; + using services; + using tolerance; + + [CommandFor("export", "exports list of currently installed packages")] + public class ChocolateyExportCommand : ICommand + { + private readonly INugetService _nugetService; + private readonly IFileSystem _fileSystem; + + public ChocolateyExportCommand(INugetService nugetService, IFileSystem fileSystem) + { + _nugetService = nugetService; + _fileSystem = fileSystem; + } + + public void configure_argument_parser(OptionSet optionSet, ChocolateyConfiguration configuration) + { + optionSet + .Add("o=|output-file-path=", + "Output File Path - the path to where the list of currently installed packages should be saved. Defaults to packages.config.", + option => configuration.ExportCommand.OutputFilePath = option.remove_surrounding_quotes()) + .Add("include-version-numbers|include-version", + "Include Version Numbers - controls whether or not version numbers for each package appear in generated file. Defaults to false.", + option => configuration.ExportCommand.IncludeVersionNumbers = option != null) + ; + } + + public void handle_additional_argument_parsing(IList unparsedArguments, ChocolateyConfiguration configuration) + { + configuration.Input = string.Join(" ", unparsedArguments); + + if (string.IsNullOrWhiteSpace(configuration.ExportCommand.OutputFilePath) && unparsedArguments.Count >=1) + { + configuration.ExportCommand.OutputFilePath = unparsedArguments[0]; + } + + // If no value has been provided for the OutputFilePath, default to packages.config + if (string.IsNullOrWhiteSpace(configuration.ExportCommand.OutputFilePath)) + { + configuration.ExportCommand.OutputFilePath = "packages.config"; + } + } + + public void handle_validation(ChocolateyConfiguration configuration) + { + // Currently, no additional validation is required. + } + + public void help_message(ChocolateyConfiguration configuration) + { + this.Log().Info(ChocolateyLoggers.Important, "Export Command"); + this.Log().Info(@" +Export all currently installed packages to a file. + +This is especially helpful when re-building a machine that was created +using Chocolatey. Export all packages to a file, and then re-install +those packages onto new machine using `choco install packages.config`. +"); + "chocolatey".Log().Info(ChocolateyLoggers.Important, "Usage"); + "chocolatey".Log().Info(@" + choco export [] +"); + + "chocolatey".Log().Info(ChocolateyLoggers.Important, "Examples"); + "chocolatey".Log().Info(@" + choco export + choco export --include-version-numbers + choco export ""'c:\temp\packages.config'"" + choco export ""'c:\temp\packages.config'"" --include-version-numbers + choco export -o=""'c:\temp\packages.config'"" + choco export -o=""'c:\temp\packages.config'"" --include-version-numbers + choco export --output-file-path=""'c:\temp\packages.config'"" + choco export --output-file-path=""'c:\temp\packages.config'"" --include-version-numbers + +NOTE: See scripting in the command reference (`choco -?`) for how to + write proper scripts and integrations. + +"); + + "chocolatey".Log().Info(ChocolateyLoggers.Important, "Exit Codes"); + "chocolatey".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. + +"); + + "chocolatey".Log().Info(ChocolateyLoggers.Important, "Options and Switches"); + } + + public bool may_require_admin_access() + { + return false; + } + + public void noop(ChocolateyConfiguration configuration) + { + this.Log().Info("Export would have been with options: {0} Output File Path={1}{0} Include Version Numbers:{2}".format_with(Environment.NewLine, configuration.ExportCommand.OutputFilePath, configuration.ExportCommand.IncludeVersionNumbers)); + } + + public void run(ChocolateyConfiguration configuration) + { + var packageResults = _nugetService.get_all_installed_packages(configuration); + var settings = new XmlWriterSettings { Indent = true, Encoding = new UTF8Encoding(false) }; + + FaultTolerance.try_catch_with_logging_exception( + () => + { + using (var stringWriter = new StringWriter()) + { + using (var xw = XmlWriter.Create(stringWriter, settings)) + { + xw.WriteProcessingInstruction("xml", "version=\"1.0\" encoding=\"utf-8\""); + xw.WriteStartElement("packages"); + + foreach (var packageResult in packageResults) + { + xw.WriteStartElement("package"); + xw.WriteAttributeString("id", packageResult.Package.Id); + + if (configuration.ExportCommand.IncludeVersionNumbers) + { + xw.WriteAttributeString("version", packageResult.Package.Version.ToString()); + } + + xw.WriteEndElement(); + } + + xw.WriteEndElement(); + xw.Flush(); + } + + var fileExists = _fileSystem.file_exists(configuration.ExportCommand.OutputFilePath); + + // If the file doesn't already exist, just write the new one out directly + if (!fileExists) + { + _fileSystem.write_file( + configuration.ExportCommand.OutputFilePath, + stringWriter.GetStringBuilder().ToString(), + new UTF8Encoding(false)); + + return; + } + + + // Otherwise, create an update file, and resiliently move it into place. + var tempUpdateFile = configuration.ExportCommand.OutputFilePath + "." + Process.GetCurrentProcess().Id + ".update"; + _fileSystem.write_file(tempUpdateFile, + stringWriter.GetStringBuilder().ToString(), + new UTF8Encoding(false)); + + _fileSystem.replace_file(tempUpdateFile, configuration.ExportCommand.OutputFilePath, configuration.ExportCommand.OutputFilePath + ".backup"); + } + }, + errorMessage: "Error exporting currently installed packages", + throwError: true + ); + } + } +} diff --git a/src/chocolatey/infrastructure.app/configuration/ChocolateyConfiguration.cs b/src/chocolatey/infrastructure.app/configuration/ChocolateyConfiguration.cs index c5cc33f73f..4f8c8679d3 100644 --- a/src/chocolatey/infrastructure.app/configuration/ChocolateyConfiguration.cs +++ b/src/chocolatey/infrastructure.app/configuration/ChocolateyConfiguration.cs @@ -50,6 +50,7 @@ public ChocolateyConfiguration() PinCommand = new PinCommandConfiguration(); OutdatedCommand = new OutdatedCommandConfiguration(); Proxy = new ProxyConfiguration(); + ExportCommand = new ExportCommandConfiguration(); #if DEBUG AllowUnofficialBuild = true; #endif @@ -334,6 +335,8 @@ private void append_output(StringBuilder propertyValues, string append) /// public OutdatedCommandConfiguration OutdatedCommand { get; set; } + public ExportCommandConfiguration ExportCommand { get; set; } + /// /// Configuration related specifically to proxies. /// @@ -383,7 +386,7 @@ public sealed class FeaturesConfiguration public bool VirusCheck { get; set; } public bool FailOnInvalidOrMissingLicense { get; set; } public bool IgnoreInvalidOptionsSwitches { get; set; } - public bool UsePackageExitCodes { get; set; } + public bool UsePackageExitCodes { get; set; } public bool UseEnhancedExitCodes { get; set; } public bool UseFipsCompliantChecksums { get; set; } public bool ShowNonElevatedWarnings { get; set; } @@ -545,4 +548,12 @@ public sealed class ProxyConfiguration public string BypassList { get; set; } public bool BypassOnLocal { get; set; } } + + [Serializable] + public sealed class ExportCommandConfiguration + { + public bool IncludeVersionNumbers { get; set; } + + public string OutputFilePath { get; set; } + } } \ No newline at end of file diff --git a/src/chocolatey/infrastructure.app/registration/ContainerBinding.cs b/src/chocolatey/infrastructure.app/registration/ContainerBinding.cs index 86d4953099..f069d964f8 100644 --- a/src/chocolatey/infrastructure.app/registration/ContainerBinding.cs +++ b/src/chocolatey/infrastructure.app/registration/ContainerBinding.cs @@ -96,7 +96,8 @@ public void RegisterComponents(Container container) new ChocolateyApiKeyCommand(container.GetInstance()), new ChocolateyUnpackSelfCommand(container.GetInstance()), new ChocolateyVersionCommand(container.GetInstance()), - new ChocolateyUpdateCommand(container.GetInstance()) + new ChocolateyUpdateCommand(container.GetInstance()), + new ChocolateyExportCommand(container.GetInstance(), container.GetInstance()) }; return list.AsReadOnly(); }, Lifestyle.Singleton); diff --git a/src/chocolatey/infrastructure.app/services/INugetService.cs b/src/chocolatey/infrastructure.app/services/INugetService.cs index bcb7179a4c..950ba408ac 100644 --- a/src/chocolatey/infrastructure.app/services/INugetService.cs +++ b/src/chocolatey/infrastructure.app/services/INugetService.cs @@ -1,13 +1,13 @@ // Copyright © 2017 - 2019 Chocolatey Software, Inc // Copyright © 2011 - 2017 RealDimensions Software, LLC -// +// // 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. @@ -17,6 +17,7 @@ namespace chocolatey.infrastructure.app.services { using System.Collections.Concurrent; + using System.Collections.Generic; using configuration; using results; @@ -58,5 +59,11 @@ public interface INugetService : ISourceRunner /// Name of the package. void remove_rollback_directory_if_exists(string packageName); + + /// + /// Get all installed packages + /// + /// The configuration + IEnumerable get_all_installed_packages(ChocolateyConfiguration config); } } diff --git a/src/chocolatey/infrastructure.app/services/NugetService.cs b/src/chocolatey/infrastructure.app/services/NugetService.cs index 0a749a7c3b..7676852284 100644 --- a/src/chocolatey/infrastructure.app/services/NugetService.cs +++ b/src/chocolatey/infrastructure.app/services/NugetService.cs @@ -1551,7 +1551,7 @@ public virtual void remove_installation_files(IPackage removedPackage, Chocolate } } - private IEnumerable get_all_installed_packages(ChocolateyConfiguration config) + public IEnumerable get_all_installed_packages(ChocolateyConfiguration config) { //todo : move to deep copy for get all installed //var listConfig = config.deep_copy();