From f5f24ad2da638bf089f2fb1b5559d6524bb4a272 Mon Sep 17 00:00:00 2001 From: Rob Reynolds Date: Sun, 3 May 2015 11:23:08 -0500 Subject: [PATCH 01/18] (GH-121) Add package files domain Define the package files classes and the package information file they will produce. --- src/chocolatey/chocolatey.csproj | 2 + .../infrastructure.app/domain/PackageFile.cs | 46 +++++++++++++ .../infrastructure.app/domain/PackageFiles.cs | 68 +++++++++++++++++++ 3 files changed, 116 insertions(+) create mode 100644 src/chocolatey/infrastructure.app/domain/PackageFile.cs create mode 100644 src/chocolatey/infrastructure.app/domain/PackageFiles.cs diff --git a/src/chocolatey/chocolatey.csproj b/src/chocolatey/chocolatey.csproj index 87f6acf0ec..2cb7f6605e 100644 --- a/src/chocolatey/chocolatey.csproj +++ b/src/chocolatey/chocolatey.csproj @@ -94,6 +94,8 @@ + + diff --git a/src/chocolatey/infrastructure.app/domain/PackageFile.cs b/src/chocolatey/infrastructure.app/domain/PackageFile.cs new file mode 100644 index 0000000000..3131601b71 --- /dev/null +++ b/src/chocolatey/infrastructure.app/domain/PackageFile.cs @@ -0,0 +1,46 @@ +// Copyright © 2011 - Present 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.domain +{ + using System; + using System.Xml.Serialization; + + /// + /// A package file in the snapshot. + /// + [Serializable] + [XmlType("file")] + public sealed class PackageFile + { + /// + /// Gets or sets the path of the file. + /// + /// + /// The path. + /// + [XmlAttribute(AttributeName = "path")] + public string Path { get; set; } + + /// + /// Gets or sets the checksum, currently only md5 because fast computation. + /// + /// + /// The checksum. + /// + [XmlAttribute(AttributeName = "checksum")] + public string Checksum { get; set; } + } +} \ No newline at end of file diff --git a/src/chocolatey/infrastructure.app/domain/PackageFiles.cs b/src/chocolatey/infrastructure.app/domain/PackageFiles.cs new file mode 100644 index 0000000000..a36ca65c1a --- /dev/null +++ b/src/chocolatey/infrastructure.app/domain/PackageFiles.cs @@ -0,0 +1,68 @@ +// Copyright © 2011 - Present 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.domain +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Xml.Serialization; + + /// + /// The package files snapshot + /// + [Serializable] + [XmlType("fileSnapshot")] + public sealed class PackageFiles + { + /// + /// Initializes a new instance of the class. + /// + public PackageFiles() + : this(new HashSet()) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The package files. + public PackageFiles(IEnumerable files) + { + Files = files != null ? files.ToList() : new List(); + } + + /// + /// Gets or sets the files. + /// + /// + /// The files. + /// + /// + /// On .NET 4.0, get error CS0200 when private set - see http://stackoverflow.com/a/23809226/18475 + /// + [XmlArray("files")] + public List Files { get; set; } + + /// + /// Gets or sets the package checksum. + /// + /// + /// The package checksum. + /// + [XmlElement(ElementName = "packageChecksum")] + public string PackageChecksum { get; set; } + } +} \ No newline at end of file From cd483be627c8359e3f2abddf4facbdd9cf1d2818 Mon Sep 17 00:00:00 2001 From: Rob Reynolds Date: Sun, 3 May 2015 11:24:55 -0500 Subject: [PATCH 02/18] (GH-121) Add files snapshot --- .../infrastructure.app/domain/ChocolateyPackageInformation.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/chocolatey/infrastructure.app/domain/ChocolateyPackageInformation.cs b/src/chocolatey/infrastructure.app/domain/ChocolateyPackageInformation.cs index 83fe777364..36ebce1296 100644 --- a/src/chocolatey/infrastructure.app/domain/ChocolateyPackageInformation.cs +++ b/src/chocolatey/infrastructure.app/domain/ChocolateyPackageInformation.cs @@ -26,6 +26,7 @@ public ChocolateyPackageInformation(IPackage package) public IPackage Package { get; set; } public Registry RegistrySnapshot { get; set; } + public PackageFiles FilesSnapshot { get; set; } public bool HasSilentUninstall { get; set; } public bool IsSideBySide { get; set; } public bool IsPinned { get; set; } From 148bf2656b04f2080b3b3c0bfc716cc4de0a327c Mon Sep 17 00:00:00 2001 From: Rob Reynolds Date: Sun, 3 May 2015 11:26:43 -0500 Subject: [PATCH 03/18] (GH-121) Add files service Add ability to read and save package file information. --- src/chocolatey/chocolatey.csproj | 2 + .../registration/ContainerBinding.cs | 1 + .../services/FilesService.cs | 48 +++++++++++++++++++ .../services/IFilesService.cs | 25 ++++++++++ 4 files changed, 76 insertions(+) create mode 100644 src/chocolatey/infrastructure.app/services/FilesService.cs create mode 100644 src/chocolatey/infrastructure.app/services/IFilesService.cs diff --git a/src/chocolatey/chocolatey.csproj b/src/chocolatey/chocolatey.csproj index 2cb7f6605e..18b9ede0fa 100644 --- a/src/chocolatey/chocolatey.csproj +++ b/src/chocolatey/chocolatey.csproj @@ -104,6 +104,8 @@ + + diff --git a/src/chocolatey/infrastructure.app/registration/ContainerBinding.cs b/src/chocolatey/infrastructure.app/registration/ContainerBinding.cs index 995a09b297..28fde79390 100644 --- a/src/chocolatey/infrastructure.app/registration/ContainerBinding.cs +++ b/src/chocolatey/infrastructure.app/registration/ContainerBinding.cs @@ -53,6 +53,7 @@ public void RegisterComponents(Container container) container.Register(Lifestyle.Singleton); container.Register(Lifestyle.Singleton); container.Register(Lifestyle.Singleton); + container.Register(Lifestyle.Singleton); container.Register(Lifestyle.Singleton); container.Register(Lifestyle.Singleton); container.Register(Lifestyle.Singleton); diff --git a/src/chocolatey/infrastructure.app/services/FilesService.cs b/src/chocolatey/infrastructure.app/services/FilesService.cs new file mode 100644 index 0000000000..c8e0adee6d --- /dev/null +++ b/src/chocolatey/infrastructure.app/services/FilesService.cs @@ -0,0 +1,48 @@ +// Copyright © 2011 - Present 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.services +{ + using domain; + using filesystem; + using infrastructure.services; + + public sealed class FilesService : IFilesService + { + private readonly IXmlService _xmlService; + private readonly IFileSystem _fileSystem; + + public FilesService(IXmlService xmlService, IFileSystem fileSystem) + { + _xmlService = xmlService; + _fileSystem = fileSystem; + } + + public PackageFiles read_from_file(string filePath) + { + if (!_fileSystem.file_exists(filePath)) + { + return null; + } + + return _xmlService.deserialize(filePath); + } + + public void save_to_file(PackageFiles snapshot, string filePath) + { + _xmlService.serialize(snapshot, filePath); + } + } +} \ No newline at end of file diff --git a/src/chocolatey/infrastructure.app/services/IFilesService.cs b/src/chocolatey/infrastructure.app/services/IFilesService.cs new file mode 100644 index 0000000000..daf42c7fcf --- /dev/null +++ b/src/chocolatey/infrastructure.app/services/IFilesService.cs @@ -0,0 +1,25 @@ +// Copyright © 2011 - Present 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.services +{ + using domain; + + public interface IFilesService + { + PackageFiles read_from_file(string filepath); + void save_to_file(PackageFiles snapshot, string filePath); + } +} \ No newline at end of file From f60ce80f20a90d29b0fc5f6ab4d2c0f39a12142d Mon Sep 17 00:00:00 2001 From: Rob Reynolds Date: Sun, 3 May 2015 11:28:06 -0500 Subject: [PATCH 04/18] (GH-121) PkgInfoService files service interaction Add the interaction to the package information service to read the files file and save to the files file. --- .../ChocolateyPackageInformationService.cs | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/chocolatey/infrastructure.app/services/ChocolateyPackageInformationService.cs b/src/chocolatey/infrastructure.app/services/ChocolateyPackageInformationService.cs index 31a5a59721..d6cf21d4e0 100644 --- a/src/chocolatey/infrastructure.app/services/ChocolateyPackageInformationService.cs +++ b/src/chocolatey/infrastructure.app/services/ChocolateyPackageInformationService.cs @@ -27,15 +27,18 @@ internal class ChocolateyPackageInformationService : IChocolateyPackageInformati { private readonly IFileSystem _fileSystem; private readonly IRegistryService _registryService; + private readonly IFilesService _filesService; private const string REGISTRY_SNAPSHOT_FILE = ".registry"; + private const string FILES_SNAPSHOT_FILE = ".files"; private const string SILENT_UNINSTALLER_FILE = ".silentUninstaller"; private const string SIDE_BY_SIDE_FILE = ".sxs"; private const string PIN_FILE = ".pin"; - public ChocolateyPackageInformationService(IFileSystem fileSystem, IRegistryService registryService) + public ChocolateyPackageInformationService(IFileSystem fileSystem, IRegistryService registryService, IFilesService filesService) { _fileSystem = fileSystem; _registryService = registryService; + _filesService = filesService; } public ChocolateyPackageInformation get_package_information(IPackage package) @@ -63,6 +66,16 @@ public ChocolateyPackageInformation get_package_information(IPackage package) logWarningInsteadOfError: true ); + FaultTolerance.try_catch_with_logging_exception( + () => + { + packageInformation.FilesSnapshot = _filesService.read_from_file(_fileSystem.combine_paths(pkgStorePath, FILES_SNAPSHOT_FILE)); + }, + "Unable to read files snapshot file", + throwError: false, + logWarningInsteadOfError: true + ); + packageInformation.HasSilentUninstall = _fileSystem.file_exists(_fileSystem.combine_paths(pkgStorePath, SILENT_UNINSTALLER_FILE)); packageInformation.IsSideBySide = _fileSystem.file_exists(_fileSystem.combine_paths(pkgStorePath, SIDE_BY_SIDE_FILE)); packageInformation.IsPinned = _fileSystem.file_exists(_fileSystem.combine_paths(pkgStorePath, PIN_FILE)); @@ -89,6 +102,11 @@ public void save_package_information(ChocolateyPackageInformation packageInforma _registryService.save_to_file(packageInformation.RegistrySnapshot, _fileSystem.combine_paths(pkgStorePath, REGISTRY_SNAPSHOT_FILE)); } + if (packageInformation.FilesSnapshot != null) + { + _filesService.save_to_file(packageInformation.FilesSnapshot, _fileSystem.combine_paths(pkgStorePath, FILES_SNAPSHOT_FILE)); + } + if (packageInformation.HasSilentUninstall) { _fileSystem.write_file(_fileSystem.combine_paths(pkgStorePath, SILENT_UNINSTALLER_FILE), string.Empty, Encoding.ASCII); From 2da7306cd6ae65928c69a8e0b85f09d2368e234d Mon Sep 17 00:00:00 2001 From: Rob Reynolds Date: Sun, 3 May 2015 15:39:33 -0500 Subject: [PATCH 05/18] (GH-121) IFileSystem.read_file_bytes Allow IFileSystem to read a file and return the file bytes as an array. --- .../infrastructure/filesystem/DotNetFileSystem.cs | 5 +++++ src/chocolatey/infrastructure/filesystem/IFileSystem.cs | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/src/chocolatey/infrastructure/filesystem/DotNetFileSystem.cs b/src/chocolatey/infrastructure/filesystem/DotNetFileSystem.cs index 41cde09571..f2dc91eaec 100644 --- a/src/chocolatey/infrastructure/filesystem/DotNetFileSystem.cs +++ b/src/chocolatey/infrastructure/filesystem/DotNetFileSystem.cs @@ -231,6 +231,11 @@ public string read_file(string filePath) return File.ReadAllText(filePath, get_file_encoding(filePath)); } + public byte[] read_file_bytes(string filePath) + { + return File.ReadAllBytes(filePath); + } + public FileStream open_file_readonly(string filePath) { return File.OpenRead(filePath); diff --git a/src/chocolatey/infrastructure/filesystem/IFileSystem.cs b/src/chocolatey/infrastructure/filesystem/IFileSystem.cs index 1595959418..959a9205c9 100644 --- a/src/chocolatey/infrastructure/filesystem/IFileSystem.cs +++ b/src/chocolatey/infrastructure/filesystem/IFileSystem.cs @@ -196,6 +196,13 @@ public interface IFileSystem /// A string of the file contents string read_file(string filePath); + /// + /// Returns the contents of a file as bytes. + /// + /// The filepath. + /// A byte array of the file contents + byte[] read_file_bytes(string filePath); + /// /// Opens a file /// @@ -330,5 +337,6 @@ public interface IFileSystem /// The path. /// The attributes. void ensure_file_attribute_set(string path, FileAttributes attributes); + } } \ No newline at end of file From 31d23f226d073d142b10e290ac8ec68e872d7b4e Mon Sep 17 00:00:00 2001 From: Rob Reynolds Date: Sun, 3 May 2015 15:43:12 -0500 Subject: [PATCH 06/18] (GH-121) Add hash provider To compute hashes in files, add a hash provider that can determine hashes of files. --- src/chocolatey/chocolatey.csproj | 2 + .../registration/ContainerBinding.cs | 3 ++ .../cryptography/IHashProvider.cs | 30 +++++++++++++ .../cryptography/Md5HashProvider.cs | 42 +++++++++++++++++++ 4 files changed, 77 insertions(+) create mode 100644 src/chocolatey/infrastructure/cryptography/IHashProvider.cs create mode 100644 src/chocolatey/infrastructure/cryptography/Md5HashProvider.cs diff --git a/src/chocolatey/chocolatey.csproj b/src/chocolatey/chocolatey.csproj index 18b9ede0fa..fcb6693aec 100644 --- a/src/chocolatey/chocolatey.csproj +++ b/src/chocolatey/chocolatey.csproj @@ -166,6 +166,8 @@ + + diff --git a/src/chocolatey/infrastructure.app/registration/ContainerBinding.cs b/src/chocolatey/infrastructure.app/registration/ContainerBinding.cs index 28fde79390..762607914c 100644 --- a/src/chocolatey/infrastructure.app/registration/ContainerBinding.cs +++ b/src/chocolatey/infrastructure.app/registration/ContainerBinding.cs @@ -20,6 +20,7 @@ namespace chocolatey.infrastructure.app.registration using SimpleInjector; using adapters; using commands; + using cryptography; using events; using filesystem; using infrastructure.commands; @@ -28,6 +29,7 @@ namespace chocolatey.infrastructure.app.registration using nuget; using services; using IFileSystem = filesystem.IFileSystem; + using IHashProvider = cryptography.IHashProvider; // ReSharper disable InconsistentNaming @@ -54,6 +56,7 @@ public void RegisterComponents(Container container) container.Register(Lifestyle.Singleton); container.Register(Lifestyle.Singleton); container.Register(Lifestyle.Singleton); + container.Register(Lifestyle.Singleton); container.Register(Lifestyle.Singleton); container.Register(Lifestyle.Singleton); container.Register(Lifestyle.Singleton); diff --git a/src/chocolatey/infrastructure/cryptography/IHashProvider.cs b/src/chocolatey/infrastructure/cryptography/IHashProvider.cs new file mode 100644 index 0000000000..5356a05097 --- /dev/null +++ b/src/chocolatey/infrastructure/cryptography/IHashProvider.cs @@ -0,0 +1,30 @@ +// Copyright © 2011 - Present 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.cryptography +{ + /// + /// A hash provider for hashing files + /// + public interface IHashProvider + { + /// + /// Returns a hash of the specified file path. + /// + /// The file path. + /// A computed hash of the file, based on the contents. + string hash_file(string filePath); + } +} \ No newline at end of file diff --git a/src/chocolatey/infrastructure/cryptography/Md5HashProvider.cs b/src/chocolatey/infrastructure/cryptography/Md5HashProvider.cs new file mode 100644 index 0000000000..e15004bc57 --- /dev/null +++ b/src/chocolatey/infrastructure/cryptography/Md5HashProvider.cs @@ -0,0 +1,42 @@ +// Copyright © 2011 - Present 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.cryptography +{ + using System; + using System.Security.Cryptography; + using filesystem; + + public sealed class Md5HashProvider : IHashProvider + { + private readonly IFileSystem _fileSystem; + private readonly MD5 _cryptoProvider; + + public Md5HashProvider(IFileSystem fileSystem) + { + _fileSystem = fileSystem; + _cryptoProvider = MD5.Create(); + } + + public string hash_file(string filePath) + { + if (!_fileSystem.file_exists(filePath)) return string.Empty; + + var hash = _cryptoProvider.ComputeHash(_fileSystem.read_file_bytes(filePath)); + + return BitConverter.ToString(hash).Replace("-", string.Empty); + } + } +} \ No newline at end of file From e27450acd474fca2362d52b6ac3613aae14dbc78 Mon Sep 17 00:00:00 2001 From: Rob Reynolds Date: Sun, 3 May 2015 16:08:28 -0500 Subject: [PATCH 07/18] (GH-121) specs for Md5HashProvider --- .../chocolatey.tests.integration.csproj | 1 + .../cryptography/Md5HashProviderSpecs.cs | 67 +++++++++++++++++++ src/chocolatey.tests/chocolatey.tests.csproj | 1 + .../cryptography/Md5HashProviderSpecs.cs | 65 ++++++++++++++++++ 4 files changed, 134 insertions(+) create mode 100644 src/chocolatey.tests.integration/infrastructure/cryptography/Md5HashProviderSpecs.cs create mode 100644 src/chocolatey.tests/infrastructure/cryptography/Md5HashProviderSpecs.cs diff --git a/src/chocolatey.tests.integration/chocolatey.tests.integration.csproj b/src/chocolatey.tests.integration/chocolatey.tests.integration.csproj index 52fd7c9a05..36d7ca3485 100644 --- a/src/chocolatey.tests.integration/chocolatey.tests.integration.csproj +++ b/src/chocolatey.tests.integration/chocolatey.tests.integration.csproj @@ -76,6 +76,7 @@ + diff --git a/src/chocolatey.tests.integration/infrastructure/cryptography/Md5HashProviderSpecs.cs b/src/chocolatey.tests.integration/infrastructure/cryptography/Md5HashProviderSpecs.cs new file mode 100644 index 0000000000..ca63b1c570 --- /dev/null +++ b/src/chocolatey.tests.integration/infrastructure/cryptography/Md5HashProviderSpecs.cs @@ -0,0 +1,67 @@ +// Copyright © 2011 - Present 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.integration.infrastructure.cryptography +{ + using System; + using System.IO; + using System.Reflection; + using System.Security.Cryptography; + using Should; + using chocolatey.infrastructure.cryptography; + using chocolatey.infrastructure.filesystem; + + public class Md5HashProviderSpecs + { + public abstract class Md5HashProviderSpecsBase : TinySpec + { + protected Md5HashProvider Provider; + protected DotNetFileSystem FileSystem; + protected string ContextDirectory; + + public override void Context() + { + FileSystem = new DotNetFileSystem(); + Provider = new Md5HashProvider(FileSystem); + ContextDirectory = FileSystem.combine_paths(FileSystem.get_directory_name(Assembly.GetExecutingAssembly().CodeBase.Replace("file:///", string.Empty)), "context"); + } + } + + public class when_Md5HashProvider_provides_a_hash : Md5HashProviderSpecsBase + { + private string result; + private string filePath; + + public override void Context() + { + base.Context(); + filePath = FileSystem.combine_paths(ContextDirectory, "testing.packages.config"); + } + + public override void Because() + { + result = Provider.hash_file(filePath); + } + + [Fact] + public void should_provide_the_correct_hash_based_on_a_checksum() + { + var expected = BitConverter.ToString(MD5.Create().ComputeHash(File.ReadAllBytes(filePath))).Replace("-", string.Empty); + + result.ShouldEqual(expected); + } + } + } +} \ No newline at end of file diff --git a/src/chocolatey.tests/chocolatey.tests.csproj b/src/chocolatey.tests/chocolatey.tests.csproj index f31a59eecf..5693af63ce 100644 --- a/src/chocolatey.tests/chocolatey.tests.csproj +++ b/src/chocolatey.tests/chocolatey.tests.csproj @@ -82,6 +82,7 @@ + diff --git a/src/chocolatey.tests/infrastructure/cryptography/Md5HashProviderSpecs.cs b/src/chocolatey.tests/infrastructure/cryptography/Md5HashProviderSpecs.cs new file mode 100644 index 0000000000..560d47255f --- /dev/null +++ b/src/chocolatey.tests/infrastructure/cryptography/Md5HashProviderSpecs.cs @@ -0,0 +1,65 @@ +// Copyright © 2011 - Present 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.cryptography +{ + using System; + using System.Security.Cryptography; + using Moq; + using Should; + using chocolatey.infrastructure.cryptography; + using chocolatey.infrastructure.filesystem; + + public class Md5HashProviderSpecs + { + public abstract class Md5HashProviderSpecsBase : TinySpec + { + protected Md5HashProvider Provider; + protected Mock FileSystem = new Mock(); + + public override void Context() + { + Provider = new Md5HashProvider(FileSystem.Object); + } + } + + public class when_Md5HashProvider_provides_a_hash : Md5HashProviderSpecsBase + { + private string result; + private string filePath = "c:\\path\\does\\not\\matter.txt"; + private readonly byte[] byteArray = new byte[] {23, 25, 27}; + + public override void Context() + { + base.Context(); + FileSystem.Setup(x => x.file_exists(It.IsAny())).Returns(true); + FileSystem.Setup(x => x.read_file_bytes(filePath)).Returns(byteArray); + } + + public override void Because() + { + result = Provider.hash_file(filePath); + } + + [Fact] + public void should_provide_the_correct_hash_based_on_a_checksum() + { + var expected = BitConverter.ToString(MD5.Create().ComputeHash(byteArray)).Replace("-", string.Empty); + + result.ShouldEqual(expected); + } + } + } +} \ No newline at end of file From a52adca01b78d723b5f01f11d1be2d5b63a2b2e6 Mon Sep 17 00:00:00 2001 From: Rob Reynolds Date: Mon, 4 May 2015 08:44:11 -0500 Subject: [PATCH 08/18] (GH-121) Capture file hashes Capture the package hashes after everything, including the install script, has had a chance to run. --- .../services/ChocolateyPackageService.cs | 9 +++++- .../services/FilesService.cs | 32 ++++++++++++++++++- .../services/IFilesService.cs | 24 ++++++++++++++ 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/src/chocolatey/infrastructure.app/services/ChocolateyPackageService.cs b/src/chocolatey/infrastructure.app/services/ChocolateyPackageService.cs index 603ddcf0bc..aa8db5f3ba 100644 --- a/src/chocolatey/infrastructure.app/services/ChocolateyPackageService.cs +++ b/src/chocolatey/infrastructure.app/services/ChocolateyPackageService.cs @@ -37,10 +37,14 @@ public class ChocolateyPackageService : IChocolateyPackageService private readonly IFileSystem _fileSystem; private readonly IRegistryService _registryService; private readonly IChocolateyPackageInformationService _packageInfoService; + private readonly IFilesService _filesService; private readonly IAutomaticUninstallerService _autoUninstallerService; private readonly IXmlService _xmlService; - public ChocolateyPackageService(INugetService nugetService, IPowershellService powershellService, IShimGenerationService shimgenService, IFileSystem fileSystem, IRegistryService registryService, IChocolateyPackageInformationService packageInfoService, IAutomaticUninstallerService autoUninstallerService, IXmlService xmlService) + public ChocolateyPackageService(INugetService nugetService, IPowershellService powershellService, + IShimGenerationService shimgenService, IFileSystem fileSystem, IRegistryService registryService, + IChocolateyPackageInformationService packageInfoService, IFilesService filesService, + IAutomaticUninstallerService autoUninstallerService, IXmlService xmlService) { _nugetService = nugetService; _powershellService = powershellService; @@ -48,6 +52,7 @@ public ChocolateyPackageService(INugetService nugetService, IPowershellService p _fileSystem = fileSystem; _registryService = registryService; _packageInfoService = packageInfoService; + _filesService = filesService; _autoUninstallerService = autoUninstallerService; _xmlService = xmlService; } @@ -186,6 +191,8 @@ public void handle_package_result(PackageResult packageResult, ChocolateyConfigu } } + pkgInfo.FilesSnapshot = _filesService.capture_package_files(packageResult, config); + if (packageResult.Success) { _shimgenService.install(config, packageResult); diff --git a/src/chocolatey/infrastructure.app/services/FilesService.cs b/src/chocolatey/infrastructure.app/services/FilesService.cs index c8e0adee6d..fff059df6d 100644 --- a/src/chocolatey/infrastructure.app/services/FilesService.cs +++ b/src/chocolatey/infrastructure.app/services/FilesService.cs @@ -15,19 +15,26 @@ namespace chocolatey.infrastructure.app.services { + using System; + using System.IO; + using configuration; + using cryptography; using domain; using filesystem; using infrastructure.services; + using results; public sealed class FilesService : IFilesService { private readonly IXmlService _xmlService; private readonly IFileSystem _fileSystem; + private readonly IHashProvider _hashProvider; - public FilesService(IXmlService xmlService, IFileSystem fileSystem) + public FilesService(IXmlService xmlService, IFileSystem fileSystem, IHashProvider hashProvider) { _xmlService = xmlService; _fileSystem = fileSystem; + _hashProvider = hashProvider; } public PackageFiles read_from_file(string filePath) @@ -44,5 +51,28 @@ public void save_to_file(PackageFiles snapshot, string filePath) { _xmlService.serialize(snapshot, filePath); } + + public PackageFiles capture_package_files(PackageResult packageResult, ChocolateyConfiguration config) + { + var installDirectory = packageResult.InstallLocation; + if (installDirectory.is_equal_to(ApplicationParameters.InstallLocation) || installDirectory.is_equal_to(ApplicationParameters.PackagesLocation)) + { + var logMessage = "Install location is not specific enough, cannot capture files:{0} Erroneous install location captured as '{1}'".format_with(Environment.NewLine, packageResult.InstallLocation); + packageResult.Messages.Add(new ResultMessage(ResultType.Warn, logMessage)); + this.Log().Error(logMessage); + return null; + } + + var packageFiles = new PackageFiles(); + + //gather all files in the folder + var files = _fileSystem.get_files(installDirectory, pattern: "*.*", option: SearchOption.AllDirectories); + foreach (string file in files.or_empty_list_if_null()) + { + packageFiles.Files.Add(new PackageFile { Path = file, Checksum = _hashProvider.hash_file(file)}); + } + + return packageFiles; + } } } \ No newline at end of file diff --git a/src/chocolatey/infrastructure.app/services/IFilesService.cs b/src/chocolatey/infrastructure.app/services/IFilesService.cs index daf42c7fcf..7900ac4735 100644 --- a/src/chocolatey/infrastructure.app/services/IFilesService.cs +++ b/src/chocolatey/infrastructure.app/services/IFilesService.cs @@ -15,11 +15,35 @@ namespace chocolatey.infrastructure.app.services { + using configuration; using domain; + using results; + /// + /// The files service for capturing and handling file snapshots. + /// public interface IFilesService { + /// + /// Read the package files file from the specified filepath. + /// + /// The filepath. + /// PackageFiles with entries based on the file if it exists, otherwise null PackageFiles read_from_file(string filepath); + + /// + /// Saves the files snapshot to the specifed file path. + /// + /// The snapshot. + /// The file path. void save_to_file(PackageFiles snapshot, string filePath); + + /// + /// Captures the snapshot of the package files + /// + /// The package result. + /// The configuration. + /// PackageFiles with entries based on the install location of the package. + PackageFiles capture_package_files(PackageResult packageResult, ChocolateyConfiguration config); } } \ No newline at end of file From 8b22eeb6abfa9dedf05afb9a9ae6f77c486eaaad Mon Sep 17 00:00:00 2001 From: Rob Reynolds Date: Mon, 4 May 2015 10:29:25 -0500 Subject: [PATCH 09/18] (GH-121) Specs and Files Service hardening - Rename Md5HashProvider to CryptoHashProvider and provide enumeration for HashAlgorithm Crytpo providers. - Implement adapter for HashAlgorithm. - Update specs for CryptoHashProvider - Specs for FilesService --- .../chocolatey.tests.integration.csproj | 2 +- ...derSpecs.cs => CrytpoHashProviderSpecs.cs} | 10 +- src/chocolatey.tests/chocolatey.tests.csproj | 3 +- .../services/FilesServiceSpecs.cs | 248 ++++++++++++++++++ .../cryptography/CrytpoHashProvider.cs | 109 ++++++++ .../cryptography/Md5HashProviderSpecs.cs | 65 ----- src/chocolatey/chocolatey.csproj | 5 +- .../ApplicationParameters.cs | 2 + .../registration/ContainerBinding.cs | 2 +- .../services/FilesService.cs | 9 +- .../infrastructure/adapters/HashAlgorithm.cs | 19 ++ .../infrastructure/adapters/IHashAlgorithm.cs | 11 + .../cryptography/CryptoHashProviderType.cs | 10 + .../cryptography/CrytpoHashProvider.cs | 79 ++++++ .../cryptography/Md5HashProvider.cs | 42 --- 15 files changed, 499 insertions(+), 117 deletions(-) rename src/chocolatey.tests.integration/infrastructure/cryptography/{Md5HashProviderSpecs.cs => CrytpoHashProviderSpecs.cs} (84%) create mode 100644 src/chocolatey.tests/infrastructure.app/services/FilesServiceSpecs.cs create mode 100644 src/chocolatey.tests/infrastructure/cryptography/CrytpoHashProvider.cs delete mode 100644 src/chocolatey.tests/infrastructure/cryptography/Md5HashProviderSpecs.cs create mode 100644 src/chocolatey/infrastructure/adapters/HashAlgorithm.cs create mode 100644 src/chocolatey/infrastructure/adapters/IHashAlgorithm.cs create mode 100644 src/chocolatey/infrastructure/cryptography/CryptoHashProviderType.cs create mode 100644 src/chocolatey/infrastructure/cryptography/CrytpoHashProvider.cs delete mode 100644 src/chocolatey/infrastructure/cryptography/Md5HashProvider.cs diff --git a/src/chocolatey.tests.integration/chocolatey.tests.integration.csproj b/src/chocolatey.tests.integration/chocolatey.tests.integration.csproj index 36d7ca3485..74e6aa82d5 100644 --- a/src/chocolatey.tests.integration/chocolatey.tests.integration.csproj +++ b/src/chocolatey.tests.integration/chocolatey.tests.integration.csproj @@ -76,7 +76,7 @@ - + diff --git a/src/chocolatey.tests.integration/infrastructure/cryptography/Md5HashProviderSpecs.cs b/src/chocolatey.tests.integration/infrastructure/cryptography/CrytpoHashProviderSpecs.cs similarity index 84% rename from src/chocolatey.tests.integration/infrastructure/cryptography/Md5HashProviderSpecs.cs rename to src/chocolatey.tests.integration/infrastructure/cryptography/CrytpoHashProviderSpecs.cs index ca63b1c570..20aa0947e7 100644 --- a/src/chocolatey.tests.integration/infrastructure/cryptography/Md5HashProviderSpecs.cs +++ b/src/chocolatey.tests.integration/infrastructure/cryptography/CrytpoHashProviderSpecs.cs @@ -23,23 +23,23 @@ namespace chocolatey.tests.integration.infrastructure.cryptography using chocolatey.infrastructure.cryptography; using chocolatey.infrastructure.filesystem; - public class Md5HashProviderSpecs + public class CrytpoHashProviderSpecs { - public abstract class Md5HashProviderSpecsBase : TinySpec + public abstract class CrytpoHashProviderSpecsBase : TinySpec { - protected Md5HashProvider Provider; + protected CrytpoHashProvider Provider; protected DotNetFileSystem FileSystem; protected string ContextDirectory; public override void Context() { FileSystem = new DotNetFileSystem(); - Provider = new Md5HashProvider(FileSystem); + Provider = new CrytpoHashProvider(FileSystem,CryptoHashProviderType.Md5); ContextDirectory = FileSystem.combine_paths(FileSystem.get_directory_name(Assembly.GetExecutingAssembly().CodeBase.Replace("file:///", string.Empty)), "context"); } } - public class when_Md5HashProvider_provides_a_hash : Md5HashProviderSpecsBase + public class when_HashProvider_provides_a_hash : CrytpoHashProviderSpecsBase { private string result; private string filePath; diff --git a/src/chocolatey.tests/chocolatey.tests.csproj b/src/chocolatey.tests/chocolatey.tests.csproj index 5693af63ce..411590c1d4 100644 --- a/src/chocolatey.tests/chocolatey.tests.csproj +++ b/src/chocolatey.tests/chocolatey.tests.csproj @@ -77,12 +77,13 @@ + - + diff --git a/src/chocolatey.tests/infrastructure.app/services/FilesServiceSpecs.cs b/src/chocolatey.tests/infrastructure.app/services/FilesServiceSpecs.cs new file mode 100644 index 0000000000..dd8bf96c41 --- /dev/null +++ b/src/chocolatey.tests/infrastructure.app/services/FilesServiceSpecs.cs @@ -0,0 +1,248 @@ +namespace chocolatey.tests.infrastructure.app.services +{ + using System; + using System.Collections.Generic; + using System.IO; + using Moq; + using Should; + using chocolatey.infrastructure.app; + using chocolatey.infrastructure.app.configuration; + using chocolatey.infrastructure.app.domain; + using chocolatey.infrastructure.app.services; + using chocolatey.infrastructure.cryptography; + using chocolatey.infrastructure.filesystem; + using chocolatey.infrastructure.results; + using chocolatey.infrastructure.services; + + public class FilesServiceSpecs + { + public abstract class FilesServiceSpecsBase : TinySpec + { + protected FilesService Service; + protected Mock XmlService = new Mock(); + protected Mock FileSystem = new Mock(); + protected Mock HashProvider = new Mock(); + + public override void Context() + { + XmlService.ResetCalls(); + FileSystem.ResetCalls(); + HashProvider.ResetCalls(); + Service = new FilesService(XmlService.Object,FileSystem.Object,HashProvider.Object); + } + } + + public class when_FilesService_reads_from_files : FilesServiceSpecsBase + { + private Func because; + + public override void Because() + { + because = () => Service.read_from_file("fake path"); + } + + [Fact] + public void should_deserialize_when_file_exists() + { + Context(); + FileSystem.Setup(x => x.file_exists(It.IsAny())).Returns(true); + XmlService.Setup(x => x.deserialize(It.IsAny())).Returns(new PackageFiles()); + + because(); + } + + [Fact] + public void should_not_deserialize_if_file_does_not_exist() + { + Context(); + FileSystem.Setup(x => x.file_exists(It.IsAny())).Returns(false); + + because(); + + XmlService.Verify(x => x.deserialize(It.IsAny()),Times.Never); + } + } + + public class when_FilesService_saves_files : FilesServiceSpecsBase + { + private Action because; + private PackageFiles files; + + public override void Because() + { + because = () => Service.save_to_file(files , "fake path"); + } + + [Fact] + public void should_save_if_the_snapshot_is_not_null() + { + Context(); + files = new PackageFiles(); + + because(); + + XmlService.Verify(x => x.serialize(files, It.IsAny()), Times.Once()); + } + + [Fact] + public void should_not_do_anything_if_the_snapshot_is_null() + { + Context(); + files = null; + + because(); + + XmlService.Verify(x => x.serialize(files, It.IsAny()), Times.Never); + } + } + + public class when_FilesService_captures_files_and_install_directory_reports_choco_install_location : FilesServiceSpecsBase + { + private PackageFiles result; + private PackageResult packageResult; + private readonly ChocolateyConfiguration config = new ChocolateyConfiguration(); + + public override void Context() + { + base.Context(); + packageResult = new PackageResult("bob", "1.2.3", ApplicationParameters.InstallLocation); + } + + public override void Because() + { + result = Service.capture_package_files(packageResult, config); + } + + [Fact] + public void should_not_call_get_files() + { + FileSystem.Verify(x => x.get_files(It.IsAny(), It.IsAny(), SearchOption.AllDirectories), Times.Never); + } + + [Fact] + public void should_return_a_warning_if_the_install_directory_matches_choco_install_location() + { + packageResult.Warning.ShouldBeTrue(); + } + + [Fact] + public void should_return_null() + { + result.ShouldBeNull(); + } + } + + public class when_FilesService_captures_files_and_install_directory_reports_packages_location : FilesServiceSpecsBase + { + private PackageFiles result; + private PackageResult packageResult; + private readonly ChocolateyConfiguration config = new ChocolateyConfiguration(); + + public override void Context() + { + base.Context(); + packageResult = new PackageResult("bob", "1.2.3", ApplicationParameters.PackagesLocation); + } + + public override void Because() + { + result = Service.capture_package_files(packageResult, config); + } + + [Fact] + public void should_not_call_get_files() + { + FileSystem.Verify(x => x.get_files(It.IsAny(), It.IsAny(), SearchOption.AllDirectories), Times.Never); + } + + [Fact] + public void should_return_a_warning_if_the_install_directory_matches_choco_install_location() + { + packageResult.Warning.ShouldBeTrue(); + } + + [Fact] + public void should_return_null() + { + result.ShouldBeNull(); + } + } + + public class when_FilesService_captures_files_and_package_result_is_null : FilesServiceSpecsBase + { + private PackageFiles result; + private PackageResult packageResult; + private readonly ChocolateyConfiguration config = new ChocolateyConfiguration(); + + public override void Context() + { + base.Context(); + packageResult = null; + } + + public override void Because() + { + result = Service.capture_package_files(packageResult, config); + } + + [Fact] + public void should_not_call_get_files() + { + FileSystem.Verify(x => x.get_files(It.IsAny(), It.IsAny(), SearchOption.AllDirectories), Times.Never); + } + + [Fact] + public void should_return_null() + { + result.ShouldBeNull(); + } + } + + public class when_FilesService_captures_files_happy_path : FilesServiceSpecsBase + { + private PackageFiles result; + private PackageResult packageResult; + private readonly ChocolateyConfiguration config = new ChocolateyConfiguration(); + private readonly string installDirectory = ApplicationParameters.PackagesLocation + "\\bob"; + private readonly IList files = new List { "file1", "file2" }; + + public override void Context() + { + base.Context(); + packageResult = new PackageResult("bob", "1.2.3", installDirectory); + + FileSystem.Setup(x => x.get_files(ApplicationParameters.PackagesLocation + "\\bob", It.IsAny(), SearchOption.AllDirectories)).Returns(files); + HashProvider.Setup(x => x.hash_file(It.IsAny())).Returns("yes"); + } + + public override void Because() + { + result = Service.capture_package_files(packageResult, config); + } + + [Fact] + public void should_return_a_PackageFiles_object() + { + result.ShouldNotBeNull(); + } + + [Fact] + public void should_contain_package_files() + { + result.Files.ShouldNotBeEmpty(); + } + + [Fact] + public void should_contain_the_correct_number_of_package_files() + { + result.Files.Count.ShouldEqual(files.Count); + } + + [Fact] + public void should_call_hash_provider_for_each_file() + { + HashProvider.Verify(x => x.hash_file(It.IsAny()),Times.Exactly(files.Count)); + } + } + } +} \ No newline at end of file diff --git a/src/chocolatey.tests/infrastructure/cryptography/CrytpoHashProvider.cs b/src/chocolatey.tests/infrastructure/cryptography/CrytpoHashProvider.cs new file mode 100644 index 0000000000..4222e4f970 --- /dev/null +++ b/src/chocolatey.tests/infrastructure/cryptography/CrytpoHashProvider.cs @@ -0,0 +1,109 @@ +// Copyright © 2011 - Present 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.cryptography +{ + using System; + using System.IO; + using System.Security.Cryptography; + using Moq; + using Should; + using chocolatey.infrastructure.adapters; + using chocolatey.infrastructure.app; + using chocolatey.infrastructure.cryptography; + using chocolatey.infrastructure.filesystem; + + public class CrytpoHashProviderSpecs + { + public abstract class CrytpoHashProviderSpecsBase : TinySpec + { + protected CrytpoHashProvider Provider; + protected Mock FileSystem = new Mock(); + + public override void Context() + { + Provider = new CrytpoHashProvider(FileSystem.Object, CryptoHashProviderType.Md5); + } + } + + public class when_HashProvider_provides_a_hash : CrytpoHashProviderSpecsBase + { + private string result; + private string filePath = "c:\\path\\does\\not\\matter.txt"; + private readonly byte[] byteArray = new byte[] {23, 25, 27}; + + public override void Context() + { + base.Context(); + FileSystem.Setup(x => x.file_exists(It.IsAny())).Returns(true); + FileSystem.Setup(x => x.read_file_bytes(filePath)).Returns(byteArray); + } + + public override void Because() + { + result = Provider.hash_file(filePath); + } + + [Fact] + public void should_provide_the_correct_hash_based_on_a_checksum() + { + var expected = BitConverter.ToString(MD5.Create().ComputeHash(byteArray)).Replace("-", string.Empty); + + result.ShouldEqual(expected); + } + } + + public class when_HashProvider_attempts_to_provide_a_hash_for_a_file_over_2GB : CrytpoHashProviderSpecsBase + { + private string result; + private string filePath = "c:\\path\\does\\not\\matter.txt"; + private readonly byte[] byteArray = new byte[] { 23, 25, 27 }; + private readonly Mock _hashAlgorithm = new Mock(); + + public override void Context() + { + base.Context(); + Provider = new CrytpoHashProvider(FileSystem.Object, _hashAlgorithm.Object); + + FileSystem.Setup(x => x.file_exists(It.IsAny())).Returns(true); + FileSystem.Setup(x => x.read_file_bytes(filePath)).Returns(byteArray); + _hashAlgorithm.Setup(x => x.ComputeHash(byteArray)).Throws(); //IO.IO_FileTooLong2GB (over Int32.MaxValue) + } + + public override void Because() + { + result = Provider.hash_file(filePath); + } + + [Fact] + public void should_log_a_warning() + { + MockLogger.MessagesFor(LogLevel.Warn).Count.ShouldEqual(1); + } + + [Fact] + public void should_not_throw_an_error_itself() + { + //this handles itself + } + + [Fact] + public void should_provide_an_unchanging_hash_for_a_file_too_big_to_hash() + { + result.ShouldEqual(ApplicationParameters.HashProviderFileTooBig); + } + } + } +} \ No newline at end of file diff --git a/src/chocolatey.tests/infrastructure/cryptography/Md5HashProviderSpecs.cs b/src/chocolatey.tests/infrastructure/cryptography/Md5HashProviderSpecs.cs deleted file mode 100644 index 560d47255f..0000000000 --- a/src/chocolatey.tests/infrastructure/cryptography/Md5HashProviderSpecs.cs +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright © 2011 - Present 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.cryptography -{ - using System; - using System.Security.Cryptography; - using Moq; - using Should; - using chocolatey.infrastructure.cryptography; - using chocolatey.infrastructure.filesystem; - - public class Md5HashProviderSpecs - { - public abstract class Md5HashProviderSpecsBase : TinySpec - { - protected Md5HashProvider Provider; - protected Mock FileSystem = new Mock(); - - public override void Context() - { - Provider = new Md5HashProvider(FileSystem.Object); - } - } - - public class when_Md5HashProvider_provides_a_hash : Md5HashProviderSpecsBase - { - private string result; - private string filePath = "c:\\path\\does\\not\\matter.txt"; - private readonly byte[] byteArray = new byte[] {23, 25, 27}; - - public override void Context() - { - base.Context(); - FileSystem.Setup(x => x.file_exists(It.IsAny())).Returns(true); - FileSystem.Setup(x => x.read_file_bytes(filePath)).Returns(byteArray); - } - - public override void Because() - { - result = Provider.hash_file(filePath); - } - - [Fact] - public void should_provide_the_correct_hash_based_on_a_checksum() - { - var expected = BitConverter.ToString(MD5.Create().ComputeHash(byteArray)).Replace("-", string.Empty); - - result.ShouldEqual(expected); - } - } - } -} \ No newline at end of file diff --git a/src/chocolatey/chocolatey.csproj b/src/chocolatey/chocolatey.csproj index fcb6693aec..f75bdca93a 100644 --- a/src/chocolatey/chocolatey.csproj +++ b/src/chocolatey/chocolatey.csproj @@ -166,8 +166,11 @@ + + + - + diff --git a/src/chocolatey/infrastructure.app/ApplicationParameters.cs b/src/chocolatey/infrastructure.app/ApplicationParameters.cs index a55cf8bbf7..f2a95be4d3 100644 --- a/src/chocolatey/infrastructure.app/ApplicationParameters.cs +++ b/src/chocolatey/infrastructure.app/ApplicationParameters.cs @@ -63,6 +63,8 @@ public static class ApplicationParameters public static int DefaultWaitForExitInSeconds = 2700; public static readonly string[] ConfigFileExtensions = new string[] {".autoconf",".config",".conf",".cfg",".jsc",".json",".jsonp",".ini",".xml",".yaml"}; + + public static string HashProviderFileTooBig = "UnableToDetectChanges_FileTooBig"; public static class Tools { diff --git a/src/chocolatey/infrastructure.app/registration/ContainerBinding.cs b/src/chocolatey/infrastructure.app/registration/ContainerBinding.cs index 762607914c..794b3463b5 100644 --- a/src/chocolatey/infrastructure.app/registration/ContainerBinding.cs +++ b/src/chocolatey/infrastructure.app/registration/ContainerBinding.cs @@ -56,7 +56,7 @@ public void RegisterComponents(Container container) container.Register(Lifestyle.Singleton); container.Register(Lifestyle.Singleton); container.Register(Lifestyle.Singleton); - container.Register(Lifestyle.Singleton); + container.Register(() => new CrytpoHashProvider(container.GetInstance(), CryptoHashProviderType.Md5), Lifestyle.Singleton); container.Register(Lifestyle.Singleton); container.Register(Lifestyle.Singleton); container.Register(Lifestyle.Singleton); diff --git a/src/chocolatey/infrastructure.app/services/FilesService.cs b/src/chocolatey/infrastructure.app/services/FilesService.cs index fff059df6d..2b87891b65 100644 --- a/src/chocolatey/infrastructure.app/services/FilesService.cs +++ b/src/chocolatey/infrastructure.app/services/FilesService.cs @@ -49,11 +49,15 @@ public PackageFiles read_from_file(string filePath) public void save_to_file(PackageFiles snapshot, string filePath) { + if (snapshot == null) return; + _xmlService.serialize(snapshot, filePath); } public PackageFiles capture_package_files(PackageResult packageResult, ChocolateyConfiguration config) { + if (packageResult == null) return null; + var installDirectory = packageResult.InstallLocation; if (installDirectory.is_equal_to(ApplicationParameters.InstallLocation) || installDirectory.is_equal_to(ApplicationParameters.PackagesLocation)) { @@ -65,11 +69,14 @@ public PackageFiles capture_package_files(PackageResult packageResult, Chocolate var packageFiles = new PackageFiles(); + this.Log().Debug(() => "Capturing package files in '{0}'".format_with(installDirectory)); //gather all files in the folder var files = _fileSystem.get_files(installDirectory, pattern: "*.*", option: SearchOption.AllDirectories); foreach (string file in files.or_empty_list_if_null()) { - packageFiles.Files.Add(new PackageFile { Path = file, Checksum = _hashProvider.hash_file(file)}); + var hash = _hashProvider.hash_file(file); + this.Log().Debug(() => " Found '{0}'{1} with checksum '{2}'".format_with(file, Environment.NewLine, hash)); + packageFiles.Files.Add(new PackageFile { Path = file, Checksum = hash}); } return packageFiles; diff --git a/src/chocolatey/infrastructure/adapters/HashAlgorithm.cs b/src/chocolatey/infrastructure/adapters/HashAlgorithm.cs new file mode 100644 index 0000000000..1c303a1cec --- /dev/null +++ b/src/chocolatey/infrastructure/adapters/HashAlgorithm.cs @@ -0,0 +1,19 @@ +namespace chocolatey.infrastructure.adapters +{ + using cryptography; + + public sealed class HashAlgorithm : IHashAlgorithm + { + private readonly System.Security.Cryptography.HashAlgorithm _algorithm; + + public HashAlgorithm(System.Security.Cryptography.HashAlgorithm algorithm) + { + _algorithm = algorithm; + } + + public byte[] ComputeHash(byte[] buffer) + { + return _algorithm.ComputeHash(buffer); + } + } +} \ No newline at end of file diff --git a/src/chocolatey/infrastructure/adapters/IHashAlgorithm.cs b/src/chocolatey/infrastructure/adapters/IHashAlgorithm.cs new file mode 100644 index 0000000000..5d87e8797e --- /dev/null +++ b/src/chocolatey/infrastructure/adapters/IHashAlgorithm.cs @@ -0,0 +1,11 @@ +namespace chocolatey.infrastructure.adapters +{ + // ReSharper disable InconsistentNaming + + public interface IHashAlgorithm + { + byte[] ComputeHash(byte[] buffer); + } + + // ReSharper restore InconsistentNaming +} \ No newline at end of file diff --git a/src/chocolatey/infrastructure/cryptography/CryptoHashProviderType.cs b/src/chocolatey/infrastructure/cryptography/CryptoHashProviderType.cs new file mode 100644 index 0000000000..77fe3352a9 --- /dev/null +++ b/src/chocolatey/infrastructure/cryptography/CryptoHashProviderType.cs @@ -0,0 +1,10 @@ +namespace chocolatey.infrastructure.cryptography +{ + public enum CryptoHashProviderType + { + Md5, + Sha1, + Sha256, + Sha512 + } +} \ No newline at end of file diff --git a/src/chocolatey/infrastructure/cryptography/CrytpoHashProvider.cs b/src/chocolatey/infrastructure/cryptography/CrytpoHashProvider.cs new file mode 100644 index 0000000000..95f7c4c0f2 --- /dev/null +++ b/src/chocolatey/infrastructure/cryptography/CrytpoHashProvider.cs @@ -0,0 +1,79 @@ +// Copyright © 2011 - Present 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.cryptography +{ + using System; + using System.IO; + using System.Security.Cryptography; + using adapters; + using app; + using filesystem; + using Environment = System.Environment; + using HashAlgorithm = adapters.HashAlgorithm; + + + public sealed class CrytpoHashProvider : IHashProvider + { + private readonly IFileSystem _fileSystem; + private readonly IHashAlgorithm _hashAlgorithm; + + public CrytpoHashProvider(IFileSystem fileSystem, CryptoHashProviderType providerType) + { + _fileSystem = fileSystem; + + switch (providerType) + { + case CryptoHashProviderType.Md5: + _hashAlgorithm = new HashAlgorithm(MD5.Create()); + break; + case CryptoHashProviderType.Sha1: + _hashAlgorithm = new HashAlgorithm(SHA1.Create()); + break; + case CryptoHashProviderType.Sha256: + _hashAlgorithm = new HashAlgorithm(SHA256.Create()); + break; + case CryptoHashProviderType.Sha512: + _hashAlgorithm = new HashAlgorithm(SHA512.Create()); + break; + } + } + + public CrytpoHashProvider(IFileSystem fileSystem, IHashAlgorithm hashAlgorithm) + { + _fileSystem = fileSystem; + _hashAlgorithm = hashAlgorithm; + } + + public string hash_file(string filePath) + { + if (!_fileSystem.file_exists(filePath)) return string.Empty; + + try + { + var hash = _hashAlgorithm.ComputeHash(_fileSystem.read_file_bytes(filePath)); + + return BitConverter.ToString(hash).Replace("-", string.Empty); + } + catch (IOException ex) + { + this.Log().Warn(() => "Error computing hash for '{0}'{1} Captured error:{1} {2}".format_with(filePath, Environment.NewLine, ex.Message)); + //IO.IO_FileTooLong2GB (over Int32.MaxValue) + return ApplicationParameters.HashProviderFileTooBig; + return "UnableToDetectChanges_FileTooBig"; + } + } + } +} \ No newline at end of file diff --git a/src/chocolatey/infrastructure/cryptography/Md5HashProvider.cs b/src/chocolatey/infrastructure/cryptography/Md5HashProvider.cs deleted file mode 100644 index e15004bc57..0000000000 --- a/src/chocolatey/infrastructure/cryptography/Md5HashProvider.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright © 2011 - Present 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.cryptography -{ - using System; - using System.Security.Cryptography; - using filesystem; - - public sealed class Md5HashProvider : IHashProvider - { - private readonly IFileSystem _fileSystem; - private readonly MD5 _cryptoProvider; - - public Md5HashProvider(IFileSystem fileSystem) - { - _fileSystem = fileSystem; - _cryptoProvider = MD5.Create(); - } - - public string hash_file(string filePath) - { - if (!_fileSystem.file_exists(filePath)) return string.Empty; - - var hash = _cryptoProvider.ComputeHash(_fileSystem.read_file_bytes(filePath)); - - return BitConverter.ToString(hash).Replace("-", string.Empty); - } - } -} \ No newline at end of file From 2fdf3bc81f9ecac67a3e9a8e2c375e77066a1bb0 Mon Sep 17 00:00:00 2001 From: Rob Reynolds Date: Tue, 12 May 2015 07:49:27 -0500 Subject: [PATCH 10/18] (GH-121) Remove unchanged files on uninstall When the files that were installed during install are unchanged, they should be removed from the package directory. Any files that have changed should stick around. --- src/chocolatey.tests/chocolatey.tests.csproj | 1 + .../services/NugetServiceSpecs.cs | 166 ++++++++++++++++++ .../services/FilesService.cs | 12 +- .../services/IFilesService.cs | 7 + .../services/NugetService.cs | 24 ++- 5 files changed, 206 insertions(+), 4 deletions(-) create mode 100644 src/chocolatey.tests/infrastructure.app/services/NugetServiceSpecs.cs diff --git a/src/chocolatey.tests/chocolatey.tests.csproj b/src/chocolatey.tests/chocolatey.tests.csproj index 411590c1d4..0afc2017fd 100644 --- a/src/chocolatey.tests/chocolatey.tests.csproj +++ b/src/chocolatey.tests/chocolatey.tests.csproj @@ -78,6 +78,7 @@ + diff --git a/src/chocolatey.tests/infrastructure.app/services/NugetServiceSpecs.cs b/src/chocolatey.tests/infrastructure.app/services/NugetServiceSpecs.cs new file mode 100644 index 0000000000..e15e31af5f --- /dev/null +++ b/src/chocolatey.tests/infrastructure.app/services/NugetServiceSpecs.cs @@ -0,0 +1,166 @@ +// Copyright © 2011 - Present 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.services +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using Moq; + using NuGet; + using chocolatey.infrastructure.app.domain; + using chocolatey.infrastructure.app.services; + using IFileSystem = chocolatey.infrastructure.filesystem.IFileSystem; + + public class NugetServiceSpecs + { + public abstract class NugetServiceSpecsBase : TinySpec + { + protected NugetService service; + protected Mock packageInfoService = new Mock(); + protected Mock fileSystem = new Mock(); + protected Mock nugetLogger = new Mock(); + protected Mock filesService = new Mock(); + protected Mock package = new Mock(); + + public override void Context() + { + fileSystem.ResetCalls(); + nugetLogger.ResetCalls(); + packageInfoService.ResetCalls(); + filesService.ResetCalls(); + package.ResetCalls(); + + service = new NugetService(fileSystem.Object, nugetLogger.Object, packageInfoService.Object, filesService.Object); + } + } + + public class when_NugetService_removes_installation_files_on_uninstall : NugetServiceSpecsBase + { + private Action because; + private ChocolateyPackageInformation packageInfo; + private const string filePath = "c:\\tests"; + private IList packageFiles; + + public override void Context() + { + base.Context(); + package.Setup(x => x.Id).Returns("bob"); + packageInfo = new ChocolateyPackageInformation(package.Object); + packageInfo.FilesSnapshot = new PackageFiles(); + packageFiles = new List(); + fileSystem.Setup(x => x.directory_exists(It.IsAny())).Returns(true); + + } + + public override void Because() + { + because = () => service.remove_installation_files(package.Object, packageInfo); + } + + [Fact] + public void should_do_nothing_if_the_directory_no_longer_exists() + { + Context(); + fileSystem.ResetCalls(); + fileSystem.Setup(x => x.directory_exists(It.IsAny())).Returns(false); + + var packageFile = new PackageFile { Path = filePath, Checksum = "1234" }; + packageFiles.Add(packageFile); + + packageInfo.FilesSnapshot.Files = packageFiles.ToList(); + + var fileSystemFiles = new List() { filePath }; + + fileSystem.Setup(x => x.get_files(It.IsAny(), It.IsAny(), SearchOption.AllDirectories)).Returns(fileSystemFiles); + filesService.Setup(x => x.get_package_file(It.IsAny())).Returns(packageFile); + + because(); + + filesService.Verify(x => x.get_package_file(It.IsAny()), Times.Never); + fileSystem.Verify(x => x.delete_file(filePath),Times.Never); + } + + [Fact] + public void should_remove_an_unchanged_file() + { + Context(); + + var packageFile = new PackageFile { Path = filePath, Checksum = "1234" }; + packageFiles.Add(packageFile); + + packageInfo.FilesSnapshot.Files = packageFiles.ToList(); + + var fileSystemFiles = new List() { filePath }; + + + fileSystem.Setup(x => x.get_files(It.IsAny(), It.IsAny(), SearchOption.AllDirectories)).Returns(fileSystemFiles); + + filesService.Setup(x => x.get_package_file(It.IsAny())).Returns(packageFile); + + because(); + + fileSystem.Verify(x => x.delete_file(filePath)); + } + + [Fact] + public void should_not_delete_a_changed_file() + { + Context(); + + var packageFile = new PackageFile { Path = filePath, Checksum = "1234" }; + var packageFileWithUpdatedChecksum = new PackageFile { Path = filePath, Checksum = "4321" }; + packageFiles.Add(packageFile); + + packageInfo.FilesSnapshot.Files = packageFiles.ToList(); + + var fileSystemFiles = new List() { filePath }; + + + fileSystem.Setup(x => x.get_files(It.IsAny(), It.IsAny(), SearchOption.AllDirectories)).Returns(fileSystemFiles); + + filesService.Setup(x => x.get_package_file(It.IsAny())).Returns(packageFileWithUpdatedChecksum); + + because(); + + fileSystem.Verify(x => x.delete_file(filePath),Times.Never); + } + + [Fact] + public void should_not_delete_an_unfound_file() + { + Context(); + + var packageFile = new PackageFile { Path = filePath, Checksum = "1234" }; + var packageFileNotInOriginal = new PackageFile { Path ="c:\\files", Checksum = "4321" }; + packageFiles.Add(packageFile); + + packageInfo.FilesSnapshot.Files = packageFiles.ToList(); + + var fileSystemFiles = new List() { filePath }; + + + fileSystem.Setup(x => x.get_files(It.IsAny(), It.IsAny(), SearchOption.AllDirectories)).Returns(fileSystemFiles); + + filesService.Setup(x => x.get_package_file(It.IsAny())).Returns(packageFileNotInOriginal); + + because(); + + fileSystem.Verify(x => x.delete_file(filePath),Times.Never); + } + } + } +} \ No newline at end of file diff --git a/src/chocolatey/infrastructure.app/services/FilesService.cs b/src/chocolatey/infrastructure.app/services/FilesService.cs index 2b87891b65..b1793c16d2 100644 --- a/src/chocolatey/infrastructure.app/services/FilesService.cs +++ b/src/chocolatey/infrastructure.app/services/FilesService.cs @@ -74,12 +74,18 @@ public PackageFiles capture_package_files(PackageResult packageResult, Chocolate var files = _fileSystem.get_files(installDirectory, pattern: "*.*", option: SearchOption.AllDirectories); foreach (string file in files.or_empty_list_if_null()) { - var hash = _hashProvider.hash_file(file); - this.Log().Debug(() => " Found '{0}'{1} with checksum '{2}'".format_with(file, Environment.NewLine, hash)); - packageFiles.Files.Add(new PackageFile { Path = file, Checksum = hash}); + packageFiles.Files.Add(get_package_file(file)); } return packageFiles; } + + public PackageFile get_package_file(string file) + { + var hash = _hashProvider.hash_file(file); + this.Log().Debug(() => " Found '{0}'{1} with checksum '{2}'".format_with(file, Environment.NewLine, hash)); + + return new PackageFile { Path = file, Checksum = hash }; + } } } \ No newline at end of file diff --git a/src/chocolatey/infrastructure.app/services/IFilesService.cs b/src/chocolatey/infrastructure.app/services/IFilesService.cs index 7900ac4735..59d2f8c20b 100644 --- a/src/chocolatey/infrastructure.app/services/IFilesService.cs +++ b/src/chocolatey/infrastructure.app/services/IFilesService.cs @@ -45,5 +45,12 @@ public interface IFilesService /// The configuration. /// PackageFiles with entries based on the install location of the package. PackageFiles capture_package_files(PackageResult packageResult, ChocolateyConfiguration config); + + /// + /// Gets a PackageFile from the filepath + /// + /// The file. + /// PackageFile object + PackageFile get_package_file(string file); } } \ No newline at end of file diff --git a/src/chocolatey/infrastructure.app/services/NugetService.cs b/src/chocolatey/infrastructure.app/services/NugetService.cs index 532ac3943d..5c8cacbbf2 100644 --- a/src/chocolatey/infrastructure.app/services/NugetService.cs +++ b/src/chocolatey/infrastructure.app/services/NugetService.cs @@ -42,6 +42,7 @@ public class NugetService : INugetService private readonly IFileSystem _fileSystem; private readonly ILogger _nugetLogger; private readonly IChocolateyPackageInformationService _packageInfoService; + private readonly IFilesService _filesService; private readonly Lazy datetime_initializer = new Lazy(() => new DateTime()); private IDateTime DateTime @@ -55,11 +56,13 @@ private IDateTime DateTime /// The file system. /// The nuget logger /// Package information service - public NugetService(IFileSystem fileSystem, ILogger nugetLogger, IChocolateyPackageInformationService packageInfoService) + /// The files service + public NugetService(IFileSystem fileSystem, ILogger nugetLogger, IChocolateyPackageInformationService packageInfoService, IFilesService filesService) { _fileSystem = fileSystem; _nugetLogger = nugetLogger; _packageInfoService = packageInfoService; + _filesService = filesService; } public void list_noop(ChocolateyConfiguration config) @@ -861,6 +864,7 @@ public ConcurrentDictionary uninstall_run(ChocolateyConfi backup_existing_version(config, packageVersion); packageManager.UninstallPackage(packageVersion, forceRemove: config.Force, removeDependencies: config.ForceDependencies); ensure_nupkg_is_removed(packageVersion, pkgInfo); + remove_installation_files(packageVersion, pkgInfo); } } catch (Exception ex) @@ -900,6 +904,24 @@ private void ensure_nupkg_is_removed(IPackage removedPackage, ChocolateyPackageI _fileSystem.delete_file(nupkg); } + public void remove_installation_files(IPackage removedPackage, ChocolateyPackageInformation pkgInfo) + { + var isSideBySide = pkgInfo != null && pkgInfo.IsSideBySide; + var installDir = _fileSystem.combine_paths(ApplicationParameters.PackagesLocation, "{0}{1}".format_with(removedPackage.Id, isSideBySide ? "." + removedPackage.Version.to_string() : string.Empty)); + + if (_fileSystem.directory_exists(installDir) && pkgInfo != null && pkgInfo.FilesSnapshot != null) + { + foreach (var file in _fileSystem.get_files(installDir, "*.*", SearchOption.AllDirectories).or_empty_list_if_null()) + { + var fileSnapshot = pkgInfo.FilesSnapshot.Files.FirstOrDefault(f => f.Path.is_equal_to(file)); + if (fileSnapshot != null && fileSnapshot.Checksum == _filesService.get_package_file(file).Checksum) + { + _fileSystem.delete_file(file); + } + } + } + } + private void set_package_names_if_all_is_specified(ChocolateyConfiguration config, Action customAction) { if (config.PackageNames.is_equal_to("all")) From b8e05f367b34ed9ea604094899faeeeb7af04288 Mon Sep 17 00:00:00 2001 From: Rob Reynolds Date: Tue, 12 May 2015 07:50:41 -0500 Subject: [PATCH 11/18] (maint) prefix "[NuGet]" on Nuget logger To see messages coming from Nuget, prefix it with "[NuGet]" so that there is no confusion where the messages are coming from. --- .../infrastructure.app/nuget/ChocolateyNugetLogger.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/chocolatey/infrastructure.app/nuget/ChocolateyNugetLogger.cs b/src/chocolatey/infrastructure.app/nuget/ChocolateyNugetLogger.cs index 2b5b8622c5..66318573c5 100644 --- a/src/chocolatey/infrastructure.app/nuget/ChocolateyNugetLogger.cs +++ b/src/chocolatey/infrastructure.app/nuget/ChocolateyNugetLogger.cs @@ -31,16 +31,16 @@ public void Log(MessageLevel level, string message, params object[] args) switch (level) { case MessageLevel.Debug: - this.Log().Debug(message, args); + this.Log().Debug("[NuGet] " + message, args); break; case MessageLevel.Info: - this.Log().Info(message, args); + this.Log().Info("[NuGet] " + message, args); break; case MessageLevel.Warning: - this.Log().Warn(message, args); + this.Log().Warn("[NuGet] " + message, args); break; case MessageLevel.Error: - this.Log().Error(message, args); + this.Log().Error("[NuGet] " + message, args); break; } } From c1d199276ab75e2ed367e37b2d771f947fca906d Mon Sep 17 00:00:00 2001 From: Rob Reynolds Date: Tue, 12 May 2015 07:50:53 -0500 Subject: [PATCH 12/18] (maint) comment --- src/chocolatey/infrastructure.app/services/NugetService.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/chocolatey/infrastructure.app/services/NugetService.cs b/src/chocolatey/infrastructure.app/services/NugetService.cs index 5c8cacbbf2..835ef7ea43 100644 --- a/src/chocolatey/infrastructure.app/services/NugetService.cs +++ b/src/chocolatey/infrastructure.app/services/NugetService.cs @@ -878,6 +878,7 @@ public ConcurrentDictionary uninstall_run(ChocolateyConfi } else { + // continue action won't be found b/c we are not actually uninstalling (this is noop) var result = packageUninstalls.GetOrAdd(packageVersion.Id.to_lower() + "." + packageVersion.Version.to_string(), new PackageResult(packageVersion, _fileSystem.combine_paths(ApplicationParameters.PackagesLocation, packageVersion.Id))); if (continueAction != null) continueAction.Invoke(result); } From 489a3422d2003892ba3b79e7e44c636824d469dd Mon Sep 17 00:00:00 2001 From: Rob Reynolds Date: Wed, 13 May 2015 11:24:26 -0500 Subject: [PATCH 13/18] (GH-121) Remove empty directories / scenarios While adding scenarios it was discovered that the empty directories were not being removed, so the service will ensure those are removed upon removing a package with no changed files in it. --- Scenarios.md | 5 ++-- src/chocolatey.tests.integration/Scenario.cs | 27 ++++++++++++++---- .../installpackage/installpackage.1.0.0.nupkg | Bin 4316 -> 4426 bytes .../tools/chocolateyinstall.ps1 | 6 +++- .../filesystem/DotNetFileSystemSpecs.cs | 2 +- .../scenarios/UninstallScenarios.cs | 8 ++++++ .../scenarios/UpgradeScenarios.cs | 20 ++++++------- .../services/NugetService.cs | 5 ++++ 8 files changed, 54 insertions(+), 19 deletions(-) diff --git a/Scenarios.md b/Scenarios.md index 2488a4de8b..95edb7a46d 100644 --- a/Scenarios.md +++ b/Scenarios.md @@ -126,7 +126,7 @@ * should install where install location reports * should not create a shim for ignored executable in the bin directory * should not create a shim for mismatched case ignored executable in the bin directory - * should not create an extensions folder for the pacakge + * should not create an extensions folder for the package * should not have inconclusive package result * should not have warning package result @@ -315,7 +315,7 @@ * should not have inconclusive package result * should not have warning package result -### ChocolateyUninstallCommand [ 12 Scenario(s), 82 Observation(s) ] +### ChocolateyUninstallCommand [ 12 Scenario(s), 83 Observation(s) ] #### when force uninstalling a package @@ -359,6 +359,7 @@ * should contain a warning message that it uninstalled successfully * should delete a shim for console in the bin directory * should delete a shim for graphical in the bin directory + * should delete any files created during the install * should delete the rollback * should have a successful package result * should not have inconclusive package result diff --git a/src/chocolatey.tests.integration/Scenario.cs b/src/chocolatey.tests.integration/Scenario.cs index 9147f0fe07..08fa4ae024 100644 --- a/src/chocolatey.tests.integration/Scenario.cs +++ b/src/chocolatey.tests.integration/Scenario.cs @@ -23,10 +23,14 @@ namespace chocolatey.tests.integration using chocolatey.infrastructure.app.configuration; using chocolatey.infrastructure.app.domain; using chocolatey.infrastructure.app.nuget; + using chocolatey.infrastructure.app.services; using chocolatey.infrastructure.filesystem; public class Scenario { + + private static IChocolateyPackageService _service; + private static readonly DotNetFileSystem _fileSystem = new DotNetFileSystem(); public static string get_top_level() @@ -79,13 +83,26 @@ public static void add_packages_to_source_location(ChocolateyConfiguration confi public static void install_package(ChocolateyConfiguration config, string packageId, string version) { - var pattern = "{0}.{1}{2}".format_with(packageId, string.IsNullOrWhiteSpace(version) ? "*" : version, Constants.PackageExtension); - var files = _fileSystem.get_files(config.Sources, pattern); - foreach (var file in files) + if (_service == null) { - var packageManager = NugetCommon.GetPackageManager(config,new ChocolateyNugetLogger(), null, null, false); - packageManager.InstallPackage(new OptimizedZipPackage(file), false,false); + _service= NUnitSetup.Container.GetInstance(); } + + var originalPackageName = config.PackageNames; + var originalPackageVersion = config.Version; + + config.PackageNames = packageId; + config.Version = version; + _service.install_run(config); + config.PackageNames = originalPackageName; + config.Version = originalPackageVersion; + //var pattern = "{0}.{1}{2}".format_with(packageId, string.IsNullOrWhiteSpace(version) ? "*" : version, Constants.PackageExtension); + //var files = _fileSystem.get_files(config.Sources, pattern); + //foreach (var file in files) + //{ + // var packageManager = NugetCommon.GetPackageManager(config, new ChocolateyNugetLogger(), null, null, false); + // packageManager.InstallPackage(new OptimizedZipPackage(file), false,false); + //} } private static ChocolateyConfiguration baseline_configuration() diff --git a/src/chocolatey.tests.integration/context/installpackage/installpackage.1.0.0.nupkg b/src/chocolatey.tests.integration/context/installpackage/installpackage.1.0.0.nupkg index 749cb1a648b058f51f6288cfa9f58e662381b32c..277f38fee29acfeba1ac437f513e3502b3123d02 100644 GIT binary patch delta 802 zcmcbkcuGkqz?+$civa{;ZP&PcR=#YyQAmkV*wD<#)G*o7!pOif#U#}rO-Vt2;z50U}E3^IbpLaQ!*nnkTH2R zvp<;r%N)iFa+~OZTSCi#?mRU)fyJ)gx7U{Ikb^+$Z-;jVI+|N<$T(~;;?P{g=qNo~ zb>@o)|2EEKVZL|fka8ufc}K$cikkP;@1JWYKQD<7XfW-_Df}=i;#${wg}q!ElAE%$ z-oB7J`XndLS313CQ*K;>`?P08nwMO@r0j8f;jy@Z{SEuH+cOq_`Cjl($#~-DxvVG7 zwVJS9R`K;~lt{hvqwz7*+a0Syi-X1QXf}U{mofbEQP}JD=1Wr1hTN!roBWui5A3B9 zRxdDpkktoF3$O)1Y4OQbY?fg5HnwmMkhf>HOj|iwl-*pOK)7$tW)EUwG@ZPUa}G%V z=2)%+Oke{*aXML**AQ%KB(DRQp2O=1rXTQnparfepN#^@-nP9ax2%A{?F7WKK#Tw` ztdl45X@j-z=aXi`uLf3ql3$uhoPF{_e#y!I`Gw_S{y)UTzyQL52uA=}g_8{h^a2S4 i3p6=GtVJaBH#!MwB|sBFSQy3BeUmo`sIk@af&>9$ru*Cg delta 687 zcmX@5bVpGrz?+$civa|R>Vn*!HvBfi&O&>Qv*|TgS2G5g5uniJnl=F=TQeYadweBUUdk ztX%+TxVUYPpUsTUK#Kgb=!hsAjlNSo;$P@7E=9}z6OpFGT zb-3n$jM{vf>i`o6$ShDePM*wb2)5ujuLGFo;&TMketaHi9$L(2qX^R0w%6pA70`=L zKr9Qy2yl&sVR9m?++;a^9bi`VT7!ddGdS# ny+8ut1x*_eHz88p8=VBT5}*S>SQyz*hJeX(f@*AnydXgU^T)9< diff --git a/src/chocolatey.tests.integration/context/installpackage/tools/chocolateyinstall.ps1 b/src/chocolatey.tests.integration/context/installpackage/tools/chocolateyinstall.ps1 index d64eb8f47b..2491151c21 100644 --- a/src/chocolatey.tests.integration/context/installpackage/tools/chocolateyinstall.ps1 +++ b/src/chocolatey.tests.integration/context/installpackage/tools/chocolateyinstall.ps1 @@ -1 +1,5 @@ -Write-Output "$env:PackageName $env:PackageVersion Installed" \ No newline at end of file +$toolsDir = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)" +"simple file" | Out-File "$toolsDir\simplefile.txt" -force + +Write-Output "$env:PackageName $env:PackageVersion Installed" + diff --git a/src/chocolatey.tests.integration/infrastructure/filesystem/DotNetFileSystemSpecs.cs b/src/chocolatey.tests.integration/infrastructure/filesystem/DotNetFileSystemSpecs.cs index 7d032c0b37..653fb137c3 100644 --- a/src/chocolatey.tests.integration/infrastructure/filesystem/DotNetFileSystemSpecs.cs +++ b/src/chocolatey.tests.integration/infrastructure/filesystem/DotNetFileSystemSpecs.cs @@ -233,7 +233,7 @@ public override void Because() { if (FileSystem.directory_exists(TestDirectory)) { - Directory.Delete(TestDirectory); + Directory.Delete(TestDirectory, recursive: true); } FileSystem.create_directory(TestDirectory); diff --git a/src/chocolatey.tests.integration/scenarios/UninstallScenarios.cs b/src/chocolatey.tests.integration/scenarios/UninstallScenarios.cs index 15b3851dce..d207d7153c 100644 --- a/src/chocolatey.tests.integration/scenarios/UninstallScenarios.cs +++ b/src/chocolatey.tests.integration/scenarios/UninstallScenarios.cs @@ -175,6 +175,14 @@ public void should_delete_a_shim_for_graphical_in_the_bin_directory() File.Exists(shimfile).ShouldBeFalse(); } + [Fact] + public void should_delete_any_files_created_during_the_install() + { + var generatedFile = Path.Combine(Scenario.get_top_level(), "lib", Configuration.PackageNames,"simplefile.txt"); + + File.Exists(generatedFile).ShouldBeFalse(); + } + [Fact] public void should_contain_a_warning_message_that_it_uninstalled_successfully() { diff --git a/src/chocolatey.tests.integration/scenarios/UpgradeScenarios.cs b/src/chocolatey.tests.integration/scenarios/UpgradeScenarios.cs index 12ae77dace..48f2e50ad8 100644 --- a/src/chocolatey.tests.integration/scenarios/UpgradeScenarios.cs +++ b/src/chocolatey.tests.integration/scenarios/UpgradeScenarios.cs @@ -1562,7 +1562,7 @@ public override void Context() string dotChocolatey = Path.Combine(Scenario.get_top_level(), ".chocolatey"); if (Directory.Exists(dotChocolatey)) { - Directory.Delete(dotChocolatey); + Directory.Delete(dotChocolatey, recursive: true); } } @@ -1584,8 +1584,8 @@ public void should_remove_the_legacy_folder_version_of_the_package() { var packageDir = Path.Combine(Scenario.get_top_level(), "lib", "isdependency.1.0.0"); Directory.Exists(packageDir).ShouldBeFalse(); - } - + } + [Fact] public void should_replace_the_legacy_folder_version_of_the_package_with_a_lib_package_folder_that_has_no_version() { @@ -1599,15 +1599,15 @@ public void should_not_upgrade_the_parent_package() var packageFile = Path.Combine(Scenario.get_top_level(), "lib", "hasdependency.1.0.0", "hasdependency.1.0.0.nupkg"); var package = new OptimizedZipPackage(packageFile); package.Version.Version.to_string().ShouldEqual("1.0.0.0"); - } - + } + [Fact] public void should_not_add_a_versionless_parent_package_folder_to_the_lib_dir() { var packageDir = Path.Combine(Scenario.get_top_level(), "lib", "hasdependency"); Directory.Exists(packageDir).ShouldBeFalse(); - } - + } + [Fact] public void should_leave_the_parent_package_as_legacy_folder() { @@ -1635,7 +1635,7 @@ public void should_not_add_a_versionless_exact_version_package_folder_to_the_lib { var packageDir = Path.Combine(Scenario.get_top_level(), "lib", "isexactversiondependency"); Directory.Exists(packageDir).ShouldBeFalse(); - } + } [Fact] public void should_contain_a_message_the_dependency_upgraded_successfully() @@ -1778,7 +1778,7 @@ public override void Context() string dotChocolatey = Path.Combine(Scenario.get_top_level(), ".chocolatey"); if (Directory.Exists(dotChocolatey)) { - Directory.Delete(dotChocolatey); + Directory.Delete(dotChocolatey, recursive: true); } } @@ -1853,7 +1853,7 @@ public void should_remove_the_legacy_folder_version_of_the_exact_version_package { var packageDir = Path.Combine(Scenario.get_top_level(), "lib", "isexactversiondependency.1.0.0"); Directory.Exists(packageDir).ShouldBeFalse(); - } + } [Fact] public void should_contain_a_message_that_everything_upgraded_successfully() diff --git a/src/chocolatey/infrastructure.app/services/NugetService.cs b/src/chocolatey/infrastructure.app/services/NugetService.cs index 835ef7ea43..34d04e26f2 100644 --- a/src/chocolatey/infrastructure.app/services/NugetService.cs +++ b/src/chocolatey/infrastructure.app/services/NugetService.cs @@ -921,6 +921,11 @@ public void remove_installation_files(IPackage removedPackage, ChocolateyPackage } } } + + if (_fileSystem.directory_exists(installDir) && !_fileSystem.get_files(installDir, "*.*", SearchOption.AllDirectories).or_empty_list_if_null().Any()) + { + _fileSystem.delete_directory_if_exists(installDir, recursive: true); + } } private void set_package_names_if_all_is_specified(ChocolateyConfiguration config, Action customAction) From 26ebf4e48e3452aa9dff9099bbbe0948d730e448 Mon Sep 17 00:00:00 2001 From: Rob Reynolds Date: Fri, 15 May 2015 06:02:41 -0500 Subject: [PATCH 14/18] (maint) renaming results to appropriate singular Results is actually a singular result, so the naming is off. Fix that to be appropriate. --- .../services/NugetService.cs | 99 ++++++++++--------- 1 file changed, 50 insertions(+), 49 deletions(-) diff --git a/src/chocolatey/infrastructure.app/services/NugetService.cs b/src/chocolatey/infrastructure.app/services/NugetService.cs index 34d04e26f2..ffb71c025f 100644 --- a/src/chocolatey/infrastructure.app/services/NugetService.cs +++ b/src/chocolatey/infrastructure.app/services/NugetService.cs @@ -268,11 +268,11 @@ public ConcurrentDictionary install_run(ChocolateyConfigu installSuccessAction: (e) => { var pkg = e.Package; - var results = packageInstalls.GetOrAdd(pkg.Id.to_lower(), new PackageResult(pkg, e.InstallPath)); - results.InstallLocation = e.InstallPath; - results.Messages.Add(new ResultMessage(ResultType.Debug, ApplicationParameters.Messages.ContinueChocolateyAction)); + var packageResult = packageInstalls.GetOrAdd(pkg.Id.to_lower(), new PackageResult(pkg, e.InstallPath)); + packageResult.InstallLocation = e.InstallPath; + packageResult.Messages.Add(new ResultMessage(ResultType.Debug, ApplicationParameters.Messages.ContinueChocolateyAction)); - if (continueAction != null) continueAction.Invoke(results); + if (continueAction != null) continueAction.Invoke(packageResult); }, uninstallSuccessAction: null, addUninstallHandler: true); @@ -289,9 +289,9 @@ public ConcurrentDictionary install_run(ChocolateyConfigu if (installedPackage != null && (version == null || version == installedPackage.Version) && !config.Force) { string logMessage = "{0} v{1} already installed.{2} Use --force to reinstall, specify a version to install, or try upgrade.".format_with(installedPackage.Id, installedPackage.Version, Environment.NewLine); - var results = packageInstalls.GetOrAdd(packageName, new PackageResult(installedPackage, _fileSystem.combine_paths(ApplicationParameters.PackagesLocation, installedPackage.Id))); - results.Messages.Add(new ResultMessage(ResultType.Warn, logMessage)); - results.Messages.Add(new ResultMessage(ResultType.Inconclusive, logMessage)); + var nullResult = packageInstalls.GetOrAdd(packageName, new PackageResult(installedPackage, _fileSystem.combine_paths(ApplicationParameters.PackagesLocation, installedPackage.Id))); + nullResult.Messages.Add(new ResultMessage(ResultType.Warn, logMessage)); + nullResult.Messages.Add(new ResultMessage(ResultType.Inconclusive, logMessage)); this.Log().Warn(ChocolateyLoggers.Important, logMessage); continue; } @@ -307,31 +307,31 @@ public ConcurrentDictionary install_run(ChocolateyConfigu { var logMessage = "{0} not installed. The package was not found with the source(s) listed.{1} If you specified a particular version and are receiving this message, it is possible that the package name exists but the version does not.{1} Version: \"{2}\"{1} Source(s): \"{3}\"".format_with(packageName, Environment.NewLine, config.Version, config.Sources); this.Log().Error(ChocolateyLoggers.Important, logMessage); - var results = packageInstalls.GetOrAdd(packageName, new PackageResult(packageName, version.to_string(), null)); - results.Messages.Add(new ResultMessage(ResultType.Error, logMessage)); + var noPkgResult = packageInstalls.GetOrAdd(packageName, new PackageResult(packageName, version.to_string(), null)); + noPkgResult.Messages.Add(new ResultMessage(ResultType.Error, logMessage)); continue; } if (installedPackage != null && (installedPackage.Version == availablePackage.Version) && config.Force) { - var results = packageInstalls.GetOrAdd(packageName, new PackageResult(installedPackage, _fileSystem.combine_paths(ApplicationParameters.PackagesLocation, installedPackage.Id))); - results.Messages.Add(new ResultMessage(ResultType.Note, "Backing up and removing old version")); + var forcedResult = packageInstalls.GetOrAdd(packageName, new PackageResult(installedPackage, _fileSystem.combine_paths(ApplicationParameters.PackagesLocation, installedPackage.Id))); + forcedResult.Messages.Add(new ResultMessage(ResultType.Note, "Backing up and removing old version")); backup_existing_version(config, installedPackage); try { packageManager.UninstallPackage(installedPackage, forceRemove: config.Force, removeDependencies: config.ForceDependencies); - if (!results.InstallLocation.is_equal_to(ApplicationParameters.PackagesLocation)) + if (!forcedResult.InstallLocation.is_equal_to(ApplicationParameters.PackagesLocation)) { - _fileSystem.delete_directory_if_exists(results.InstallLocation, recursive: true); + _fileSystem.delete_directory_if_exists(forcedResult.InstallLocation, recursive: true); } } catch (Exception ex) { string logMessage = "{0}:{1} {2}".format_with("Unable to remove existing package prior to forced reinstall", Environment.NewLine, ex.Message); this.Log().Warn(logMessage); - results.Messages.Add(new ResultMessage(ResultType.Inconclusive, logMessage)); + forcedResult.Messages.Add(new ResultMessage(ResultType.Inconclusive, logMessage)); } } @@ -350,9 +350,9 @@ public ConcurrentDictionary install_run(ChocolateyConfigu { var logMessage = "{0} not installed. An error occurred during installation:{1} {2}".format_with(packageName, Environment.NewLine, ex.Message); this.Log().Error(ChocolateyLoggers.Important, logMessage); - var results = packageInstalls.GetOrAdd(packageName, new PackageResult(packageName, version.to_string(), null)); - results.Messages.Add(new ResultMessage(ResultType.Error, logMessage)); - if (continueAction != null) continueAction.Invoke(results); + var errorResult = packageInstalls.GetOrAdd(packageName, new PackageResult(packageName, version.to_string(), null)); + errorResult.Messages.Add(new ResultMessage(ResultType.Error, logMessage)); + if (continueAction != null) continueAction.Invoke(errorResult); } } @@ -403,11 +403,11 @@ public ConcurrentDictionary upgrade_run(ChocolateyConfigu installSuccessAction: (e) => { var pkg = e.Package; - var results = packageInstalls.GetOrAdd(pkg.Id.to_lower(), new PackageResult(pkg, e.InstallPath)); - results.InstallLocation = e.InstallPath; - results.Messages.Add(new ResultMessage(ResultType.Debug, ApplicationParameters.Messages.ContinueChocolateyAction)); + var packageResult = packageInstalls.GetOrAdd(pkg.Id.to_lower(), new PackageResult(pkg, e.InstallPath)); + packageResult.InstallLocation = e.InstallPath; + packageResult.Messages.Add(new ResultMessage(ResultType.Debug, ApplicationParameters.Messages.ContinueChocolateyAction)); - if (continueAction != null) continueAction.Invoke(results); + if (continueAction != null) continueAction.Invoke(packageResult); }, uninstallSuccessAction: null, addUninstallHandler: false); @@ -426,8 +426,8 @@ public ConcurrentDictionary upgrade_run(ChocolateyConfigu { //todo v1 Deprecation - reimplement error //string logMessage = "{0} is not installed. Cannot upgrade a non-existent package.".format_with(packageName); - //var results = packageInstalls.GetOrAdd(packageName, new PackageResult(packageName, null, null)); - //results.Messages.Add(new ResultMessage(ResultType.Error, logMessage)); + //var result = packageInstalls.GetOrAdd(packageName, new PackageResult(packageName, null, null)); + //result.Messages.Add(new ResultMessage(ResultType.Error, logMessage)); //if (config.RegularOutput) this.Log().Error(ChocolateyLoggers.Important, logMessage); //continue; @@ -450,9 +450,9 @@ packages as of version 1.0.0. That is what the install command is for. else { var installResults = install_run(config, continueAction); - foreach (var packageResult in installResults) + foreach (var result in installResults) { - packageInstalls.GetOrAdd(packageResult.Key, packageResult.Value); + packageInstalls.GetOrAdd(result.Key, result.Value); } } @@ -468,17 +468,17 @@ packages as of version 1.0.0. That is what the install command is for. if (availablePackage == null) { string logMessage = "{0} was not found with the source(s) listed.{1} If you specified a particular version and are receiving this message, it is possible that the package name exists but the version does not.{1} Version: \"{2}\"{1} Source(s): \"{3}\"".format_with(packageName, Environment.NewLine, config.Version, config.Sources); - var unfoundResults = packageInstalls.GetOrAdd(packageName, new PackageResult(packageName, version.to_string(), null)); + var unfoundResult = packageInstalls.GetOrAdd(packageName, new PackageResult(packageName, version.to_string(), null)); if (config.UpgradeCommand.FailOnUnfound) { - unfoundResults.Messages.Add(new ResultMessage(ResultType.Error, logMessage)); + unfoundResult.Messages.Add(new ResultMessage(ResultType.Error, logMessage)); if (config.RegularOutput) this.Log().Error(ChocolateyLoggers.Important, logMessage); } else { - unfoundResults.Messages.Add(new ResultMessage(ResultType.Warn, logMessage)); - unfoundResults.Messages.Add(new ResultMessage(ResultType.Inconclusive, logMessage)); + unfoundResult.Messages.Add(new ResultMessage(ResultType.Warn, logMessage)); + unfoundResult.Messages.Add(new ResultMessage(ResultType.Inconclusive, logMessage)); if (config.RegularOutput) { this.Log().Warn(ChocolateyLoggers.Important, logMessage); @@ -498,12 +498,12 @@ packages as of version 1.0.0. That is what the install command is for. //todo: get smarter about realizing multiple versions have been installed before and allowing that } - var results = packageInstalls.GetOrAdd(packageName, new PackageResult(availablePackage, _fileSystem.combine_paths(ApplicationParameters.PackagesLocation, availablePackage.Id))); + var packageResult = packageInstalls.GetOrAdd(packageName, new PackageResult(availablePackage, _fileSystem.combine_paths(ApplicationParameters.PackagesLocation, availablePackage.Id))); if ((installedPackage.Version > availablePackage.Version)) { string logMessage = "{0} v{1} is newer than the most recent.{2} You must be smarter than the average bear...".format_with(installedPackage.Id, installedPackage.Version, Environment.NewLine); - results.Messages.Add(new ResultMessage(ResultType.Inconclusive, logMessage)); + packageResult.Messages.Add(new ResultMessage(ResultType.Inconclusive, logMessage)); if (config.RegularOutput) { @@ -522,9 +522,9 @@ packages as of version 1.0.0. That is what the install command is for. if (!config.Force) { - if (results.Messages.Count((p) => p.Message == ApplicationParameters.Messages.ContinueChocolateyAction) == 0) + if (packageResult.Messages.Count((p) => p.Message == ApplicationParameters.Messages.ContinueChocolateyAction) == 0) { - results.Messages.Add(new ResultMessage(ResultType.Inconclusive, logMessage)); + packageResult.Messages.Add(new ResultMessage(ResultType.Inconclusive, logMessage)); } if (config.RegularOutput) @@ -539,7 +539,7 @@ packages as of version 1.0.0. That is what the install command is for. continue; } - results.Messages.Add(new ResultMessage(ResultType.Note, logMessage)); + packageResult.Messages.Add(new ResultMessage(ResultType.Note, logMessage)); if (config.RegularOutput) this.Log().Info(logMessage); } @@ -548,7 +548,7 @@ packages as of version 1.0.0. That is what the install command is for. if (availablePackage.Version > installedPackage.Version) { string logMessage = "You have {0} v{1} installed. Version {2} is available based on your source(s)".format_with(installedPackage.Id, installedPackage.Version, availablePackage.Version); - results.Messages.Add(new ResultMessage(ResultType.Note, logMessage)); + packageResult.Messages.Add(new ResultMessage(ResultType.Note, logMessage)); if (config.RegularOutput) { @@ -563,8 +563,8 @@ packages as of version 1.0.0. That is what the install command is for. if (isPinned) { string logMessage = "{0} is pinned. Skipping pinned package.".format_with(packageName); - results.Messages.Add(new ResultMessage(ResultType.Warn, logMessage)); - results.Messages.Add(new ResultMessage(ResultType.Inconclusive, logMessage)); + packageResult.Messages.Add(new ResultMessage(ResultType.Warn, logMessage)); + packageResult.Messages.Add(new ResultMessage(ResultType.Inconclusive, logMessage)); if (config.RegularOutput) this.Log().Warn(ChocolateyLoggers.Important, logMessage); continue; @@ -598,8 +598,8 @@ packages as of version 1.0.0. That is what the install command is for. { var logMessage = "{0} not upgraded. An error occurred during installation:{1} {2}".format_with(packageName, Environment.NewLine, ex.Message); this.Log().Error(ChocolateyLoggers.Important, logMessage); - results.Messages.Add(new ResultMessage(ResultType.Error, logMessage)); - if (continueAction != null) continueAction.Invoke(results); + packageResult.Messages.Add(new ResultMessage(ResultType.Error, logMessage)); + if (continueAction != null) continueAction.Invoke(packageResult); } } } @@ -733,11 +733,12 @@ public ConcurrentDictionary uninstall_run(ChocolateyConfi var pkg = e.Package; // this section fires twice sometimes, like for older packages in a sxs install... - var results = packageUninstalls.GetOrAdd(pkg.Id.to_lower() + "." + pkg.Version.to_string(), new PackageResult(pkg, e.InstallPath)); + var packageResult = packageUninstalls.GetOrAdd(pkg.Id.to_lower() + "." + pkg.Version.to_string(), new PackageResult(pkg, e.InstallPath)); + packageResult.InstallLocation = e.InstallPath; string logMessage = "{0}{1} v{2}{3}".format_with(Environment.NewLine, pkg.Id, pkg.Version.to_string(), config.Force ? " (forced)" : string.Empty); - if (results.Messages.Count((p) => p.Message == ApplicationParameters.Messages.NugetEventActionHeader) == 0) + if (packageResult.Messages.Count((p) => p.Message == ApplicationParameters.Messages.NugetEventActionHeader) == 0) { - results.Messages.Add(new ResultMessage(ResultType.Debug, ApplicationParameters.Messages.NugetEventActionHeader)); + packageResult.Messages.Add(new ResultMessage(ResultType.Debug, ApplicationParameters.Messages.NugetEventActionHeader)); "chocolatey".Log().Info(ChocolateyLoggers.Important, logMessage); loopCount = 0; } @@ -757,8 +758,8 @@ public ConcurrentDictionary uninstall_run(ChocolateyConfi var latestVersion = packageManager.LocalRepository.FindPackage(e.Package.Id); if (latestVersion.Version == pkg.Version || config.AllowMultipleVersions) { - results.Messages.Add(new ResultMessage(ResultType.Debug, ApplicationParameters.Messages.ContinueChocolateyAction)); - if (continueAction != null) continueAction.Invoke(results); + packageResult.Messages.Add(new ResultMessage(ResultType.Debug, ApplicationParameters.Messages.ContinueChocolateyAction)); + if (continueAction != null) continueAction.Invoke(packageResult); } else { @@ -792,8 +793,8 @@ public ConcurrentDictionary uninstall_run(ChocolateyConfi if (installedPackageVersions.Count == 0) { string logMessage = "{0} is not installed. Cannot uninstall a non-existent package.".format_with(packageName); - var results = packageUninstalls.GetOrAdd(packageName, new PackageResult(packageName, null, null)); - results.Messages.Add(new ResultMessage(ResultType.Error, logMessage)); + var missingResult = packageUninstalls.GetOrAdd(packageName, new PackageResult(packageName, null, null)); + missingResult.Messages.Add(new ResultMessage(ResultType.Error, logMessage)); if (config.RegularOutput) this.Log().Error(ChocolateyLoggers.Important, logMessage); continue; @@ -844,9 +845,9 @@ public ConcurrentDictionary uninstall_run(ChocolateyConfi if (pkgInfo != null && pkgInfo.IsPinned) { string logMessage = "{0} is pinned. Skipping pinned package.".format_with(packageName); - var pinnedResults = packageUninstalls.GetOrAdd(packageName, new PackageResult(packageName, null, null)); - pinnedResults.Messages.Add(new ResultMessage(ResultType.Warn, logMessage)); - pinnedResults.Messages.Add(new ResultMessage(ResultType.Inconclusive, logMessage)); + var pinnedResult = packageUninstalls.GetOrAdd(packageName, new PackageResult(packageName, null, null)); + pinnedResult.Messages.Add(new ResultMessage(ResultType.Warn, logMessage)); + pinnedResult.Messages.Add(new ResultMessage(ResultType.Inconclusive, logMessage)); if (config.RegularOutput) this.Log().Warn(ChocolateyLoggers.Important, logMessage); continue; } From 7f7b277ce437cb80901de5cd0fd26133a321f64b Mon Sep 17 00:00:00 2001 From: Rob Reynolds Date: Fri, 15 May 2015 06:03:45 -0500 Subject: [PATCH 15/18] (GH-121) Remove package checksum The entire package checksum is not needed at this time so remove it. --- src/chocolatey/infrastructure.app/domain/PackageFiles.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/chocolatey/infrastructure.app/domain/PackageFiles.cs b/src/chocolatey/infrastructure.app/domain/PackageFiles.cs index a36ca65c1a..f7f53b20d1 100644 --- a/src/chocolatey/infrastructure.app/domain/PackageFiles.cs +++ b/src/chocolatey/infrastructure.app/domain/PackageFiles.cs @@ -56,13 +56,5 @@ public PackageFiles(IEnumerable files) [XmlArray("files")] public List Files { get; set; } - /// - /// Gets or sets the package checksum. - /// - /// - /// The package checksum. - /// - [XmlElement(ElementName = "packageChecksum")] - public string PackageChecksum { get; set; } } } \ No newline at end of file From 40b443712e49cbd2cb5c9988c227835f2dea9421 Mon Sep 17 00:00:00 2001 From: Rob Reynolds Date: Fri, 15 May 2015 06:05:28 -0500 Subject: [PATCH 16/18] (GH-121) IFilesService capture files by directory --- .../infrastructure.app/services/FilesService.cs | 17 +++++++++++++++-- .../services/IFilesService.cs | 8 ++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/chocolatey/infrastructure.app/services/FilesService.cs b/src/chocolatey/infrastructure.app/services/FilesService.cs index b1793c16d2..462e7246af 100644 --- a/src/chocolatey/infrastructure.app/services/FilesService.cs +++ b/src/chocolatey/infrastructure.app/services/FilesService.cs @@ -67,11 +67,23 @@ public PackageFiles capture_package_files(PackageResult packageResult, Chocolate return null; } + return capture_package_files(installDirectory, config); + } + + public PackageFiles capture_package_files(string directory, ChocolateyConfiguration config) + { + if (directory.is_equal_to(ApplicationParameters.InstallLocation) || directory.is_equal_to(ApplicationParameters.PackagesLocation)) + { + var logMessage = "Install location is not specific enough, cannot capture files:{0} Erroneous install location captured as '{1}'".format_with(Environment.NewLine, directory); + this.Log().Error(logMessage); + return null; + } + var packageFiles = new PackageFiles(); - this.Log().Debug(() => "Capturing package files in '{0}'".format_with(installDirectory)); + this.Log().Debug(() => "Capturing package files in '{0}'".format_with(directory)); //gather all files in the folder - var files = _fileSystem.get_files(installDirectory, pattern: "*.*", option: SearchOption.AllDirectories); + var files = _fileSystem.get_files(directory, pattern: "*.*", option: SearchOption.AllDirectories); foreach (string file in files.or_empty_list_if_null()) { packageFiles.Files.Add(get_package_file(file)); @@ -80,6 +92,7 @@ public PackageFiles capture_package_files(PackageResult packageResult, Chocolate return packageFiles; } + public PackageFile get_package_file(string file) { var hash = _hashProvider.hash_file(file); diff --git a/src/chocolatey/infrastructure.app/services/IFilesService.cs b/src/chocolatey/infrastructure.app/services/IFilesService.cs index 59d2f8c20b..09687a0eaf 100644 --- a/src/chocolatey/infrastructure.app/services/IFilesService.cs +++ b/src/chocolatey/infrastructure.app/services/IFilesService.cs @@ -46,6 +46,14 @@ public interface IFilesService /// PackageFiles with entries based on the install location of the package. PackageFiles capture_package_files(PackageResult packageResult, ChocolateyConfiguration config); + /// + /// Captures the snapshot of the package files + /// + /// The directory. + /// The configuration. + /// PackageFiles with entries based on the install location of the package. + PackageFiles capture_package_files(string directory, ChocolateyConfiguration config); + /// /// Gets a PackageFile from the filepath /// From 9a68ebd084cb6334c8b5e094994f7003cc8d13e6 Mon Sep 17 00:00:00 2001 From: Rob Reynolds Date: Fri, 15 May 2015 07:52:42 -0500 Subject: [PATCH 17/18] (GH-121) backup changed files When files have changed in a package, back those files up prior to upgrade. --- .../services/FilesServiceSpecs.cs | 10 ++- .../services/NugetServiceSpecs.cs | 76 ++++++++++++++++--- .../services/FilesService.cs | 10 +-- .../services/NugetService.cs | 49 +++++++++--- 4 files changed, 115 insertions(+), 30 deletions(-) diff --git a/src/chocolatey.tests/infrastructure.app/services/FilesServiceSpecs.cs b/src/chocolatey.tests/infrastructure.app/services/FilesServiceSpecs.cs index dd8bf96c41..5f4b471d9b 100644 --- a/src/chocolatey.tests/infrastructure.app/services/FilesServiceSpecs.cs +++ b/src/chocolatey.tests/infrastructure.app/services/FilesServiceSpecs.cs @@ -192,9 +192,15 @@ public void should_not_call_get_files() } [Fact] - public void should_return_null() + public void should_return_a_non_null_object() { - result.ShouldBeNull(); + result.ShouldNotBeNull(); + } + + [Fact] + public void should_return_empty_package_files() + { + result.Files.ShouldBeEmpty(); } } diff --git a/src/chocolatey.tests/infrastructure.app/services/NugetServiceSpecs.cs b/src/chocolatey.tests/infrastructure.app/services/NugetServiceSpecs.cs index e15e31af5f..1f89ade817 100644 --- a/src/chocolatey.tests/infrastructure.app/services/NugetServiceSpecs.cs +++ b/src/chocolatey.tests/infrastructure.app/services/NugetServiceSpecs.cs @@ -21,6 +21,7 @@ namespace chocolatey.tests.infrastructure.app.services using System.Linq; using Moq; using NuGet; + using chocolatey.infrastructure.app.configuration; using chocolatey.infrastructure.app.domain; using chocolatey.infrastructure.app.services; using IFileSystem = chocolatey.infrastructure.filesystem.IFileSystem; @@ -48,6 +49,70 @@ public override void Context() } } + public class when_NugetService_backs_up_changed_files : NugetServiceSpecsBase + { + private Action because; + private ChocolateyPackageInformation packageInfo; + private const string filePath = "c:\\tests"; + private PackageFiles packageFiles; + private readonly ChocolateyConfiguration config = new ChocolateyConfiguration(); + + public override void Context() + { + base.Context(); + package.Setup(x => x.Id).Returns("bob"); + packageInfo = new ChocolateyPackageInformation(package.Object); + packageInfo.FilesSnapshot = new PackageFiles(); + packageFiles = new PackageFiles(); + fileSystem.Setup(x => x.directory_exists(It.IsAny())).Returns(true); + } + + public override void Because() + { + because = () => service.backup_changed_files(filePath, config, packageInfo); + } + + [Fact] + public void should_ignore_an_unchanged_file() + { + Context(); + + var packageFile = new PackageFile { Path = filePath, Checksum = "1234" }; + packageFiles.Files.Add(packageFile); + packageInfo.FilesSnapshot = packageFiles; + + var fileSystemFiles = new List() { filePath }; + fileSystem.Setup(x => x.get_files(It.IsAny(), It.IsAny(), SearchOption.AllDirectories)).Returns(fileSystemFiles); + filesService.Setup(x => x.capture_package_files(It.IsAny(),config)).Returns(packageFiles); + + because(); + + fileSystem.Verify(x => x.copy_file(It.IsAny(),It.IsAny(),It.IsAny()),Times.Never); + } + + [Fact] + public void should_backup_a_changed_file() + { + Context(); + + var packageFile = new PackageFile { Path = filePath, Checksum = "1234" }; + packageFiles.Files.Add(packageFile); + packageInfo.FilesSnapshot = packageFiles; + + var packageFileWithUpdatedChecksum = new PackageFile { Path = filePath, Checksum = "4321" }; + + var fileSystemFiles = new List() { filePath }; + fileSystem.Setup(x => x.get_files(It.IsAny(), It.IsAny(), SearchOption.AllDirectories)).Returns(fileSystemFiles); + var updatedPackageFiles = new PackageFiles(); + updatedPackageFiles.Files = new List{packageFileWithUpdatedChecksum}; + filesService.Setup(x => x.capture_package_files(It.IsAny(), config)).Returns(updatedPackageFiles); + + because(); + + fileSystem.Verify(x => x.copy_file(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + } + } + public class when_NugetService_removes_installation_files_on_uninstall : NugetServiceSpecsBase { private Action because; @@ -63,7 +128,6 @@ public override void Context() packageInfo.FilesSnapshot = new PackageFiles(); packageFiles = new List(); fileSystem.Setup(x => x.directory_exists(It.IsAny())).Returns(true); - } public override void Because() @@ -80,11 +144,9 @@ public void should_do_nothing_if_the_directory_no_longer_exists() var packageFile = new PackageFile { Path = filePath, Checksum = "1234" }; packageFiles.Add(packageFile); - packageInfo.FilesSnapshot.Files = packageFiles.ToList(); var fileSystemFiles = new List() { filePath }; - fileSystem.Setup(x => x.get_files(It.IsAny(), It.IsAny(), SearchOption.AllDirectories)).Returns(fileSystemFiles); filesService.Setup(x => x.get_package_file(It.IsAny())).Returns(packageFile); @@ -101,14 +163,10 @@ public void should_remove_an_unchanged_file() var packageFile = new PackageFile { Path = filePath, Checksum = "1234" }; packageFiles.Add(packageFile); - packageInfo.FilesSnapshot.Files = packageFiles.ToList(); var fileSystemFiles = new List() { filePath }; - - fileSystem.Setup(x => x.get_files(It.IsAny(), It.IsAny(), SearchOption.AllDirectories)).Returns(fileSystemFiles); - filesService.Setup(x => x.get_package_file(It.IsAny())).Returns(packageFile); because(); @@ -124,14 +182,10 @@ public void should_not_delete_a_changed_file() var packageFile = new PackageFile { Path = filePath, Checksum = "1234" }; var packageFileWithUpdatedChecksum = new PackageFile { Path = filePath, Checksum = "4321" }; packageFiles.Add(packageFile); - packageInfo.FilesSnapshot.Files = packageFiles.ToList(); var fileSystemFiles = new List() { filePath }; - - fileSystem.Setup(x => x.get_files(It.IsAny(), It.IsAny(), SearchOption.AllDirectories)).Returns(fileSystemFiles); - filesService.Setup(x => x.get_package_file(It.IsAny())).Returns(packageFileWithUpdatedChecksum); because(); diff --git a/src/chocolatey/infrastructure.app/services/FilesService.cs b/src/chocolatey/infrastructure.app/services/FilesService.cs index 462e7246af..86099c88d8 100644 --- a/src/chocolatey/infrastructure.app/services/FilesService.cs +++ b/src/chocolatey/infrastructure.app/services/FilesService.cs @@ -56,7 +56,7 @@ public void save_to_file(PackageFiles snapshot, string filePath) public PackageFiles capture_package_files(PackageResult packageResult, ChocolateyConfiguration config) { - if (packageResult == null) return null; + if (packageResult == null) return new PackageFiles(); var installDirectory = packageResult.InstallLocation; if (installDirectory.is_equal_to(ApplicationParameters.InstallLocation) || installDirectory.is_equal_to(ApplicationParameters.PackagesLocation)) @@ -72,15 +72,15 @@ public PackageFiles capture_package_files(PackageResult packageResult, Chocolate public PackageFiles capture_package_files(string directory, ChocolateyConfiguration config) { + var packageFiles = new PackageFiles(); + if (directory.is_equal_to(ApplicationParameters.InstallLocation) || directory.is_equal_to(ApplicationParameters.PackagesLocation)) { var logMessage = "Install location is not specific enough, cannot capture files:{0} Erroneous install location captured as '{1}'".format_with(Environment.NewLine, directory); this.Log().Error(logMessage); - return null; + return packageFiles; } - - var packageFiles = new PackageFiles(); - + this.Log().Debug(() => "Capturing package files in '{0}'".format_with(directory)); //gather all files in the folder var files = _fileSystem.get_files(directory, pattern: "*.*", option: SearchOption.AllDirectories); diff --git a/src/chocolatey/infrastructure.app/services/NugetService.cs b/src/chocolatey/infrastructure.app/services/NugetService.cs index ffb71c025f..d19bef25ed 100644 --- a/src/chocolatey/infrastructure.app/services/NugetService.cs +++ b/src/chocolatey/infrastructure.app/services/NugetService.cs @@ -317,7 +317,7 @@ public ConcurrentDictionary install_run(ChocolateyConfigu var forcedResult = packageInstalls.GetOrAdd(packageName, new PackageResult(installedPackage, _fileSystem.combine_paths(ApplicationParameters.PackagesLocation, installedPackage.Id))); forcedResult.Messages.Add(new ResultMessage(ResultType.Note, "Backing up and removing old version")); - backup_existing_version(config, installedPackage); + backup_existing_version(config, installedPackage, _packageInfoService.get_package_information(installedPackage)); try { @@ -580,7 +580,7 @@ packages as of version 1.0.0. That is what the install command is for. version == null ? null : version.ToString())) { rename_legacy_package_version(config, installedPackage, pkgInfo); - backup_existing_version(config, installedPackage); + backup_existing_version(config, installedPackage, pkgInfo); if (config.Force && (installedPackage.Version == availablePackage.Version)) { FaultTolerance.try_catch_with_logging_exception( @@ -627,7 +627,7 @@ public void rename_legacy_package_version(ChocolateyConfiguration config, IPacka } } - public void backup_existing_version(ChocolateyConfiguration config, IPackage installedPackage) + public void backup_existing_version(ChocolateyConfiguration config, IPackage installedPackage, ChocolateyPackageInformation packageInfo) { _fileSystem.create_directory_if_not_exists(ApplicationParameters.PackageBackupLocation); @@ -672,7 +672,7 @@ public void backup_existing_version(ChocolateyConfiguration config, IPackage ins } } - backup_configuration_files(pkgInstallPath, installedPackage.Version.to_string()); + backup_changed_files(pkgInstallPath, config, packageInfo); if (errored) { @@ -685,16 +685,41 @@ process locking the folder or files. Please make sure nothing is } } - private void backup_configuration_files(string packageInstallPath, string version) + public void backup_changed_files(string packageInstallPath, ChocolateyConfiguration config, ChocolateyPackageInformation packageInfo) { - var configFiles = _fileSystem.get_files(packageInstallPath, ApplicationParameters.ConfigFileExtensions, SearchOption.AllDirectories); - foreach (var file in configFiles.or_empty_list_if_null()) + if (packageInfo == null || packageInfo.Package == null) return; + + var version = packageInfo.Package.Version.to_string(); + + if (packageInfo.FilesSnapshot == null || packageInfo.FilesSnapshot.Files.Count == 0) { - var backupName = "{0}.{1}".format_with(_fileSystem.get_file_name(file), version); + var configFiles = _fileSystem.get_files(packageInstallPath, ApplicationParameters.ConfigFileExtensions, SearchOption.AllDirectories); + foreach (var file in configFiles.or_empty_list_if_null()) + { + var backupName = "{0}.{1}".format_with(_fileSystem.get_file_name(file), version); - FaultTolerance.try_catch_with_logging_exception( - () => _fileSystem.copy_file(file, _fileSystem.combine_paths(_fileSystem.get_directory_name(file), backupName), overwriteExisting: true), - "Error backing up configuration file"); + FaultTolerance.try_catch_with_logging_exception( + () => _fileSystem.copy_file(file, _fileSystem.combine_paths(_fileSystem.get_directory_name(file), backupName), overwriteExisting: true), + "Error backing up configuration file"); + } + } + else + { + var currentFiles = _filesService.capture_package_files(packageInstallPath, config); + foreach (var currentFile in currentFiles.Files.or_empty_list_if_null()) + { + var installedFile = packageInfo.FilesSnapshot.Files.FirstOrDefault(x => x.Path.is_equal_to(currentFile.Path)); + if (installedFile != null) + { + if (!currentFile.Checksum.is_equal_to(installedFile.Checksum)) + { + var backupName = "{0}.{1}".format_with(_fileSystem.get_file_name(currentFile.Path), version); + FaultTolerance.try_catch_with_logging_exception( + () => _fileSystem.copy_file(currentFile.Path, _fileSystem.combine_paths(_fileSystem.get_directory_name(currentFile.Path), backupName), overwriteExisting: true), + "Error backing up changed file"); + } + } + } } } @@ -862,7 +887,7 @@ public ConcurrentDictionary uninstall_run(ChocolateyConfi version == null ? null : version.ToString())) { rename_legacy_package_version(config, packageVersion, pkgInfo); - backup_existing_version(config, packageVersion); + backup_existing_version(config, packageVersion, pkgInfo); packageManager.UninstallPackage(packageVersion, forceRemove: config.Force, removeDependencies: config.ForceDependencies); ensure_nupkg_is_removed(packageVersion, pkgInfo); remove_installation_files(packageVersion, pkgInfo); From ea3e73597774dc762bf0e94bce5c988ef910c258 Mon Sep 17 00:00:00 2001 From: Rob Reynolds Date: Fri, 15 May 2015 07:53:44 -0500 Subject: [PATCH 18/18] (maint) harden removing install files Add fault tolerance around deleting the nupkg with an error. Add fault tolerance around deleting the rest of the files on the system. --- .../infrastructure.app/services/NugetService.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/chocolatey/infrastructure.app/services/NugetService.cs b/src/chocolatey/infrastructure.app/services/NugetService.cs index d19bef25ed..b768005590 100644 --- a/src/chocolatey/infrastructure.app/services/NugetService.cs +++ b/src/chocolatey/infrastructure.app/services/NugetService.cs @@ -927,8 +927,11 @@ private void ensure_nupkg_is_removed(IPackage removedPackage, ChocolateyPackageI var nupkgFile = "{0}{1}.nupkg".format_with(removedPackage.Id, isSideBySide ? "." + removedPackage.Version.to_string() : string.Empty); var installDir = _fileSystem.combine_paths(ApplicationParameters.PackagesLocation, "{0}{1}".format_with(removedPackage.Id, isSideBySide ? "." + removedPackage.Version.to_string() : string.Empty)); var nupkg = _fileSystem.combine_paths(installDir, nupkgFile); - - _fileSystem.delete_file(nupkg); + + FaultTolerance.try_catch_with_logging_exception( + () => _fileSystem.delete_file(nupkg), + "Error deleting nupkg file", + throwError: true); } public void remove_installation_files(IPackage removedPackage, ChocolateyPackageInformation pkgInfo) @@ -943,7 +946,9 @@ public void remove_installation_files(IPackage removedPackage, ChocolateyPackage var fileSnapshot = pkgInfo.FilesSnapshot.Files.FirstOrDefault(f => f.Path.is_equal_to(file)); if (fileSnapshot != null && fileSnapshot.Checksum == _filesService.get_package_file(file).Checksum) { - _fileSystem.delete_file(file); + FaultTolerance.try_catch_with_logging_exception( + () => _fileSystem.delete_file(file), + "Error deleting file"); } } }