winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit d6493dd2d27c61c888ab0f816c84c7bed6704954
parent ffa142f19e61172ebe9edbfe1a7eca7250b1cf1b
Author: JohnMcPMS <johnmcp@microsoft.com>
Date:   Thu, 20 Feb 2020 14:10:21 -0800

Add update and remove commands and implementations (#39)


Diffstat:
Msrc/AppInstallerCLICore/Commands/SourceCommand.cpp | 100+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Msrc/AppInstallerCLICore/Commands/SourceCommand.h | 26++++++++++++++++++++++++++
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj | 1+
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters | 3+++
Msrc/AppInstallerCLITests/Sources.cpp | 174+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Asrc/AppInstallerCLITests/TestHooks.h | 26++++++++++++++++++++++++++
Msrc/AppInstallerCLITests/main.cpp | 6+-----
Msrc/AppInstallerCommonCore/AppInstallerStrings.cpp | 6+++---
Msrc/AppInstallerCommonCore/Public/AppInstallerErrors.h | 1+
Msrc/AppInstallerCommonCore/Public/AppInstallerStrings.h | 6+++---
Msrc/AppInstallerCommonCore/Runtime.cpp | 6++++++
Msrc/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj | 3+++
Msrc/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters | 9+++++++++
Msrc/AppInstallerRepositoryCore/Manifest/Manifest.h | 4+++-
Msrc/AppInstallerRepositoryCore/Manifest/ManifestInstaller.h | 4+++-
Asrc/AppInstallerRepositoryCore/Microsoft/PreIndexedSource.cpp | 48++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerRepositoryCore/Microsoft/PreIndexedSource.h | 39+++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerRepositoryCore/Public/AppInstallerRepositorySource.h | 20++++++++++++--------
Msrc/AppInstallerRepositoryCore/RepositorySource.cpp | 157++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Asrc/AppInstallerRepositoryCore/SourceFactory.h | 25+++++++++++++++++++++++++
Msrc/AppInstallerRepositoryCore/pch.h | 5+++++
21 files changed, 622 insertions(+), 47 deletions(-)

diff --git a/src/AppInstallerCLICore/Commands/SourceCommand.cpp b/src/AppInstallerCLICore/Commands/SourceCommand.cpp @@ -18,6 +18,8 @@ namespace AppInstaller::CLI return InitializeFromMoveOnly<std::vector<std::unique_ptr<Command>>>({ std::make_unique<SourceAddCommand>(), std::make_unique<SourceListCommand>(), + std::make_unique<SourceUpdateCommand>(), + std::make_unique<SourceRemoveCommand>(), }); } @@ -42,8 +44,8 @@ namespace AppInstaller::CLI { return { Argument{ s_SourceCommand_ArgName_Name, LOCME("Name of the source for future reference"), ArgumentType::Positional, true }, - Argument{ s_SourceCommand_ArgName_Type, LOCME("Type of the source"), ArgumentType::Positional, true }, Argument{ s_SourceCommand_ArgName_Arg, LOCME("Argument given to the source"), ArgumentType::Positional, true }, + Argument{ s_SourceCommand_ArgName_Type, LOCME("Type of the source"), ArgumentType::Positional, false }, }; } @@ -62,13 +64,20 @@ namespace AppInstaller::CLI void SourceAddCommand::ExecuteInternal(Invocation& inv, std::ostream& out, std::istream&) const { std::string name = *inv.GetArg(s_SourceCommand_ArgName_Name); - std::string type = *inv.GetArg(s_SourceCommand_ArgName_Type); std::string arg = *inv.GetArg(s_SourceCommand_ArgName_Arg); + std::string type; + if (inv.Contains(s_SourceCommand_ArgName_Type)) + { + type = *inv.GetArg(s_SourceCommand_ArgName_Type); + } out << LOCME("Adding source:") << std::endl; - out << " " << name << std::endl; - out << " " << type << std::endl; - out << " " << arg << std::endl; + out << " " << LOCME("Name: ") << name << std::endl; + out << " " << LOCME("Arg: ") << arg << std::endl; + if (!type.empty()) + { + out << " " << LOCME("Type: ") << type << std::endl; + } // TODO: Needs to be hooked up to a reporter when real source construction happens. Repository::AddSource(std::move(name), std::move(type), std::move(arg)); @@ -141,4 +150,85 @@ namespace AppInstaller::CLI } } } + + std::vector<Argument> SourceUpdateCommand::GetArguments() const + { + return { + Argument{ s_SourceCommand_ArgName_Name, LOCME("Name of the source to update"), ArgumentType::Positional, false }, + }; + } + + std::string SourceUpdateCommand::ShortDescription() const + { + return LOCME("Update current sources"); + } + + std::vector<std::string> SourceUpdateCommand::GetLongDescription() const + { + return { + LOCME("Update current sources"), + }; + } + + void SourceUpdateCommand::ExecuteInternal(Invocation& inv, std::ostream& out, std::istream&) const + { + if (inv.Contains(s_SourceCommand_ArgName_Name)) + { + const std::string& name = *inv.GetArg(s_SourceCommand_ArgName_Name); + out << LOCME("Updating source: ") << name << "..." << std::endl; + if (!Repository::UpdateSource(name)) + { + out << LOCME("Could not find a source by that name.") << std::endl; + } + else + { + out << LOCME("Done") << std::endl; + } + } + else + { + out << LOCME("Updating all sources...") << std::endl; + + std::vector<Repository::SourceDetails> sources = Repository::GetSources(); + for (const auto& sd : sources) + { + out << LOCME(" Updating source: ") << sd.Name << "..." << std::flush; + Repository::UpdateSource(sd.Name); + out << LOCME(" Done.") << std::endl; + } + } + } + + std::vector<Argument> SourceRemoveCommand::GetArguments() const + { + return { + Argument{ s_SourceCommand_ArgName_Name, LOCME("Name of the source to update"), ArgumentType::Positional, true }, + }; + } + + std::string SourceRemoveCommand::ShortDescription() const + { + return LOCME("Remove current sources"); + } + + std::vector<std::string> SourceRemoveCommand::GetLongDescription() const + { + return { + LOCME("Remove current sources"), + }; + } + + void SourceRemoveCommand::ExecuteInternal(Invocation& inv, std::ostream& out, std::istream&) const + { + const std::string& name = *inv.GetArg(s_SourceCommand_ArgName_Name); + out << LOCME("Removing source: ") << name << "..." << std::endl; + if (!Repository::RemoveSource(name)) + { + out << LOCME("Could not find a source by that name.") << std::endl; + } + else + { + out << LOCME("Done") << std::endl; + } + } } diff --git a/src/AppInstallerCLICore/Commands/SourceCommand.h b/src/AppInstallerCLICore/Commands/SourceCommand.h @@ -43,4 +43,30 @@ namespace AppInstaller::CLI protected: virtual void ExecuteInternal(Invocation& inv, std::ostream& out, std::istream& in) const; }; + + struct SourceUpdateCommand final : public Command + { + SourceUpdateCommand() : Command("update") {} + + virtual std::vector<Argument> GetArguments() const override; + + virtual std::string ShortDescription() const override; + virtual std::vector<std::string> GetLongDescription() const override; + + protected: + virtual void ExecuteInternal(Invocation& inv, std::ostream& out, std::istream& in) const; + }; + + struct SourceRemoveCommand final : public Command + { + SourceRemoveCommand() : Command("remove") {} + + virtual std::vector<Argument> GetArguments() const override; + + virtual std::string ShortDescription() const override; + virtual std::vector<std::string> GetLongDescription() const override; + + protected: + virtual void ExecuteInternal(Invocation& inv, std::ostream& out, std::istream& in) const; + }; } diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -148,6 +148,7 @@ <ItemGroup> <ClInclude Include="pch.h" /> <ClInclude Include="TestCommon.h" /> + <ClInclude Include="TestHooks.h" /> </ItemGroup> <ItemGroup> <ClCompile Include="Downloader.cpp" /> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -24,6 +24,9 @@ <ClInclude Include="TestCommon.h"> <Filter>Header Files</Filter> </ClInclude> + <ClInclude Include="TestHooks.h"> + <Filter>Header Files</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> diff --git a/src/AppInstallerCLITests/Sources.cpp b/src/AppInstallerCLITests/Sources.cpp @@ -2,6 +2,7 @@ // Licensed under the MIT License. #include "pch.h" #include "TestCommon.h" +#include "TestHooks.h" #include <Public/AppInstallerRepositorySource.h> #include <AppInstallerDateTime.h> @@ -64,6 +65,70 @@ Sources: LastUpdate: 0 )"sv; +// Helper to create a simple source. +struct TestSource : public ISource +{ + TestSource() = default; + TestSource(const SourceDetails& details) : m_details(details) {} + + static std::unique_ptr<ISource> Create(const SourceDetails& details) + { + return std::make_unique<TestSource>(details); + } + + // ISource + const SourceDetails& GetDetails() const override + { + return m_details; + } + + SearchResult Search(const SearchRequest& request) const override + { + UNREFERENCED_PARAMETER(request); + return {}; + } + + SourceDetails m_details; +}; + +// Helper that allows some lambdas to be wrapped into a source factory. +struct TestSourceFactory : public ISourceFactory +{ + using CreateFunctor = std::function<std::unique_ptr<ISource>(const SourceDetails&)>; + using UpdateFunctor = std::function<void(SourceDetails&)>; + using RemoveFunctor = std::function<void(const SourceDetails&)>; + + TestSourceFactory() : + m_Create(TestSource::Create), m_Update([](SourceDetails&) {}), m_Remove([](const SourceDetails&) {}) {} + + // ISourceFactory + std::unique_ptr<ISource> Create(const SourceDetails& details) override + { + return m_Create(details); + } + + void Update(SourceDetails& details) override + { + m_Update(details); + } + + void Remove(const SourceDetails& details) override + { + m_Remove(details); + } + + // Make copies of self when requested. + operator std::function<std::unique_ptr<ISourceFactory>()>() + { + return [this]() { return std::make_unique<TestSourceFactory>(*this); }; + } + + CreateFunctor m_Create; + UpdateFunctor m_Update; + RemoveFunctor m_Remove; +}; + + TEST_CASE("RepoSources_UserSettingDoesNotExist", "[sources]") { RemoveSetting(s_RepositorySettings_UserSources); @@ -131,12 +196,21 @@ TEST_CASE("RepoSources_MissingField", "[sources]") TEST_CASE("RepoSources_AddSource", "[sources]") { RemoveSetting(s_RepositorySettings_UserSources); + TestHook_ClearSourceFactoryOverrides(); std::string name = "thisIsTheName"; std::string type = "thisIsTheType"; std::string arg = "thisIsTheArg"; + std::string data = "thisIsTheData"; + + bool updateCalledOnFactory = false; + TestSourceFactory factory; + factory.m_Update = [&](SourceDetails& sd) { updateCalledOnFactory = true; sd.Data = data; }; + TestHook_SetSourceFactoryOverride(type, factory); - auto source = AddSource(name, type, arg); + AddSource(name, type, arg); + + REQUIRE(updateCalledOnFactory); std::vector<SourceDetails> sources = GetSources(); REQUIRE(sources.size() == 1); @@ -144,7 +218,7 @@ TEST_CASE("RepoSources_AddSource", "[sources]") REQUIRE(sources[0].Name == name); REQUIRE(sources[0].Type == type); REQUIRE(sources[0].Arg == arg); - REQUIRE(sources[0].Data == ""); + REQUIRE(sources[0].Data == data); REQUIRE(sources[0].LastUpdateTime == ConvertUnixEpochToSystemClock(0)); } @@ -155,8 +229,15 @@ TEST_CASE("RepoSources_AddMultipleSources", "[sources]") std::string name = "thisIsTheName"; std::string type = "thisIsTheType"; std::string arg = "thisIsTheArg"; + std::string data = "thisIsTheData"; - auto source = AddSource(name, type, arg); + const char* suffix[2] = { "", "2" }; + + TestSourceFactory factory1; + factory1.m_Update = [&](SourceDetails& sd) { sd.Data = data; }; + TestHook_SetSourceFactoryOverride(type, factory1); + + AddSource(name, type, arg); std::vector<SourceDetails> sources = GetSources(); REQUIRE(sources.size() == 1); @@ -164,12 +245,14 @@ TEST_CASE("RepoSources_AddMultipleSources", "[sources]") REQUIRE(sources[0].Name == name); REQUIRE(sources[0].Type == type); REQUIRE(sources[0].Arg == arg); - REQUIRE(sources[0].Data == ""); + REQUIRE(sources[0].Data == data); REQUIRE(sources[0].LastUpdateTime == ConvertUnixEpochToSystemClock(0)); - const char* suffix[2] = { "", "2" }; + TestSourceFactory factory2; + factory2.m_Update = [&](SourceDetails& sd) { sd.Data = data + suffix[1]; }; + TestHook_SetSourceFactoryOverride(type + suffix[1], factory2); - source = AddSource(name + suffix[1], type + suffix[1], arg + suffix[1]); + AddSource(name + suffix[1], type + suffix[1], arg + suffix[1]); sources = GetSources(); REQUIRE(sources.size() == 2); @@ -180,7 +263,84 @@ TEST_CASE("RepoSources_AddMultipleSources", "[sources]") REQUIRE(sources[i].Name == name + suffix[i]); REQUIRE(sources[i].Type == type + suffix[i]); REQUIRE(sources[i].Arg == arg + suffix[i]); - REQUIRE(sources[i].Data == ""); + REQUIRE(sources[i].Data == data + suffix[i]); REQUIRE(sources[i].LastUpdateTime == ConvertUnixEpochToSystemClock(0)); } } + +TEST_CASE("RepoSources_UpdateSource", "[sources]") +{ + using namespace std::chrono_literals; + + RemoveSetting(s_RepositorySettings_UserSources); + TestHook_ClearSourceFactoryOverrides(); + + std::string name = "thisIsTheName"; + std::string type = "thisIsTheType"; + std::string arg = "thisIsTheArg"; + std::string data = "thisIsTheData"; + + bool updateCalledOnFactory = false; + TestSourceFactory factory; + factory.m_Update = [&](SourceDetails& sd) { updateCalledOnFactory = true; sd.Data = data; }; + TestHook_SetSourceFactoryOverride(type, factory); + + AddSource(name, type, arg); + + REQUIRE(updateCalledOnFactory); + + std::vector<SourceDetails> sources = GetSources(); + REQUIRE(sources.size() == 1); + + REQUIRE(sources[0].Name == name); + REQUIRE(sources[0].Type == type); + REQUIRE(sources[0].Arg == arg); + REQUIRE(sources[0].Data == data); + REQUIRE(sources[0].LastUpdateTime == ConvertUnixEpochToSystemClock(0)); + + // Reset for a call to update + updateCalledOnFactory = false; + auto now = std::chrono::system_clock::now(); + factory.m_Update = [&](SourceDetails& sd) { updateCalledOnFactory = true; sd.LastUpdateTime = now; }; + + UpdateSource(name); + + REQUIRE(updateCalledOnFactory); + + sources = GetSources(); + REQUIRE(sources.size() == 1); + + REQUIRE(sources[0].Name == name); + REQUIRE(sources[0].Type == type); + REQUIRE(sources[0].Arg == arg); + REQUIRE(sources[0].Data == data); + REQUIRE((now - sources[0].LastUpdateTime) < 1s); +} + +TEST_CASE("RepoSources_RemoveSource", "[sources]") +{ + RemoveSetting(s_RepositorySettings_UserSources); + TestHook_ClearSourceFactoryOverrides(); + + std::string name = "thisIsTheName"; + std::string type = "thisIsTheType"; + std::string arg = "thisIsTheArg"; + std::string data = "thisIsTheData"; + + bool removeCalledOnFactory = false; + TestSourceFactory factory; + factory.m_Remove = [&](const SourceDetails&) { removeCalledOnFactory = true; }; + TestHook_SetSourceFactoryOverride(type, factory); + + AddSource(name, type, arg); + + std::vector<SourceDetails> sources = GetSources(); + REQUIRE(sources.size() == 1); + + RemoveSource(name); + + REQUIRE(removeCalledOnFactory); + + sources = GetSources(); + REQUIRE(sources.empty()); +} diff --git a/src/AppInstallerCLITests/TestHooks.h b/src/AppInstallerCLITests/TestHooks.h @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include <SourceFactory.h> +#include <filesystem> +#include <functional> +#include <memory> +#include <string> + +#ifdef AICLI_DISABLE_TEST_HOOKS +static_assert(false, "Test hooks have been disabled"); +#endif + +namespace AppInstaller +{ + namespace Runtime + { + void TestHook_ForceContainerPrepend(const std::filesystem::path& prepend); + } + + namespace Repository + { + void TestHook_SetSourceFactoryOverride(const std::string& type, std::function<std::unique_ptr<ISourceFactory>()>&& factory); + void TestHook_ClearSourceFactoryOverrides(); + } +} diff --git a/src/AppInstallerCLITests/main.cpp b/src/AppInstallerCLITests/main.cpp @@ -10,16 +10,12 @@ #include <Public/AppInstallerTelemetry.h> #include "TestCommon.h" +#include "TestHooks.h" using namespace winrt; using namespace Windows::Foundation; using namespace std::string_literals; -namespace AppInstaller::Runtime -{ - void TestHook_ForceContainerPrepend(const std::filesystem::path& prepend); -} - // Logs the the AppInstaller log target to break up individual tests struct LoggingBreakListener : public Catch::TestEventListenerBase diff --git a/src/AppInstallerCommonCore/AppInstallerStrings.cpp b/src/AppInstallerCommonCore/AppInstallerStrings.cpp @@ -5,7 +5,7 @@ namespace AppInstaller::Utility { - bool CaseInsensitiveEquals(const std::string& a, const std::string& b) + bool CaseInsensitiveEquals(std::string_view a, std::string_view b) { // TODO: When we bring in ICU, do this correctly. return ToLower(a) == ToLower(b); @@ -41,7 +41,7 @@ namespace AppInstaller::Utility return result; } - std::string ToLower(const std::string& in) + std::string ToLower(std::string_view in) { std::string result(in); std::transform(result.begin(), result.end(), result.begin(), @@ -49,7 +49,7 @@ namespace AppInstaller::Utility return result; } - std::wstring ToLower(const std::wstring& in) + std::wstring ToLower(std::wstring_view in) { std::wstring result(in); std::transform(result.begin(), result.end(), result.begin(), diff --git a/src/AppInstallerCommonCore/Public/AppInstallerErrors.h b/src/AppInstallerCommonCore/Public/AppInstallerErrors.h @@ -17,3 +17,4 @@ #define APPINSTALLER_CLI_ERROR_INDEX_INTEGRITY_COMPROMISED ((HRESULT)0x8A15000A) #define APPINSTALLER_CLI_ERROR_SOURCES_INVALID ((HRESULT)0x8A15000B) #define APPINSTALLER_CLI_ERROR_SOURCE_NAME_ALREADY_EXISTS ((HRESULT)0x8A15000C) +#define APPINSTALLER_CLI_ERROR_INVALID_SOURCE_TYPE ((HRESULT)0x8A15000D) diff --git a/src/AppInstallerCommonCore/Public/AppInstallerStrings.h b/src/AppInstallerCommonCore/Public/AppInstallerStrings.h @@ -8,7 +8,7 @@ namespace AppInstaller::Utility { // Compares the two UTF8 strings in a case insensitive manner. - bool CaseInsensitiveEquals(const std::string& a, const std::string& b); + bool CaseInsensitiveEquals(std::string_view a, std::string_view b); // Converts the given UTF16 string to UTF8 std::string ConvertToUTF8(std::wstring_view input); @@ -17,10 +17,10 @@ namespace AppInstaller::Utility std::wstring ConvertToUTF16(std::string_view input); // Get the lower case version of the given std::string - std::string ToLower(const std::string& in); + std::string ToLower(std::string_view in); // Get the lower case version of the given std::wstring - std::wstring ToLower(const std::wstring& in); + std::wstring ToLower(std::wstring_view in); // Checks if the input string is empty or whitespace bool IsEmptyOrWhitespace(std::wstring_view str); diff --git a/src/AppInstallerCommonCore/Runtime.cpp b/src/AppInstallerCommonCore/Runtime.cpp @@ -16,7 +16,9 @@ namespace AppInstaller::Runtime return (result != APPMODEL_ERROR_NO_PACKAGE); } +#ifndef AICLI_DISABLE_TEST_HOOKS static std::filesystem::path s_Settings_TestHook_ForcedContainerPrepend; +#endif void ValidateSettingNamePath(std::filesystem::path& name) { @@ -24,12 +26,14 @@ namespace AppInstaller::Runtime THROW_HR_IF(E_INVALIDARG, name.has_root_path()); THROW_HR_IF(E_INVALIDARG, !name.has_filename()); +#ifndef AICLI_DISABLE_TEST_HOOKS if (!s_Settings_TestHook_ForcedContainerPrepend.empty()) { std::filesystem::path result = s_Settings_TestHook_ForcedContainerPrepend; result /= name; name = std::move(result); } +#endif } // Gets the container within LocalSettings for the given path. @@ -226,8 +230,10 @@ namespace AppInstaller::Runtime } } +#ifndef AICLI_DISABLE_TEST_HOOKS void TestHook_ForceContainerPrepend(const std::filesystem::path& prepend) { s_Settings_TestHook_ForcedContainerPrepend = prepend; } +#endif } diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj @@ -171,6 +171,7 @@ <ClInclude Include="Manifest\Manifest.h" /> <ClInclude Include="Manifest\ManifestInstaller.h" /> <ClInclude Include="Manifest\ManifestLocalization.h" /> + <ClInclude Include="Microsoft\PreIndexedSource.h" /> <ClInclude Include="Microsoft\Schema\1_0\ChannelTable.h" /> <ClInclude Include="Microsoft\Schema\1_0\CommandsTable.h" /> <ClInclude Include="Microsoft\Schema\1_0\IdTable.h" /> @@ -188,6 +189,7 @@ <ClInclude Include="Microsoft\Schema\Version.h" /> <ClInclude Include="Microsoft\SQLiteIndex.h" /> <ClInclude Include="pch.h" /> + <ClInclude Include="SourceFactory.h" /> <ClInclude Include="SQLiteStatementBuilder.h" /> <ClInclude Include="Public\AppInstallerRepositorySearch.h" /> <ClInclude Include="Public\AppInstallerRepositorySource.h" /> @@ -197,6 +199,7 @@ <ClCompile Include="Manifest\Manifest.cpp" /> <ClCompile Include="Manifest\ManifestInstaller.cpp" /> <ClCompile Include="Manifest\ManifestLocalization.cpp" /> + <ClCompile Include="Microsoft\PreIndexedSource.cpp" /> <ClCompile Include="Microsoft\Schema\1_0\Interface.cpp" /> <ClCompile Include="Microsoft\Schema\1_0\ManifestTable.cpp" /> <ClCompile Include="Microsoft\Schema\1_0\OneToManyTable.cpp" /> diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters @@ -102,6 +102,12 @@ <ClInclude Include="Public\AppInstallerRepositorySearch.h"> <Filter>Public</Filter> </ClInclude> + <ClInclude Include="Microsoft\PreIndexedSource.h"> + <Filter>Microsoft</Filter> + </ClInclude> + <ClInclude Include="SourceFactory.h"> + <Filter>Header Files</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -149,6 +155,9 @@ <ClCompile Include="RepositorySource.cpp"> <Filter>Source Files</Filter> </ClCompile> + <ClCompile Include="Microsoft\PreIndexedSource.cpp"> + <Filter>Microsoft</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerRepositoryCore/Manifest/Manifest.h b/src/AppInstallerRepositoryCore/Manifest/Manifest.h @@ -1,10 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #pragma once - #include "AppInstallerErrors.h" #include "ManifestInstaller.h" #include "ManifestLocalization.h" +#include <yaml-cpp/yaml.h> + +#include <wil/result.h> #include <filesystem> #include <string> diff --git a/src/AppInstallerRepositoryCore/Manifest/ManifestInstaller.h b/src/AppInstallerRepositoryCore/Manifest/ManifestInstaller.h @@ -1,9 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #pragma once +#include <AppInstallerArchitecture.h> +#include <yaml-cpp/yaml.h> + #include <string> #include <map> -#include <AppInstallerArchitecture.h> namespace AppInstaller::Manifest { diff --git a/src/AppInstallerRepositoryCore/Microsoft/PreIndexedSource.cpp b/src/AppInstallerRepositoryCore/Microsoft/PreIndexedSource.cpp @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "pch.h" +#include "Microsoft/PreIndexedSource.h" + +namespace AppInstaller::Repository::Microsoft +{ + namespace + { + struct PreIndexedSourceFactory : public ISourceFactory + { + std::unique_ptr<ISource> Create(const SourceDetails& details) override + { + UNREFERENCED_PARAMETER(details); + THROW_HR(E_NOTIMPL); + } + + void Update(SourceDetails& details) override + { + UNREFERENCED_PARAMETER(details); + THROW_HR(E_NOTIMPL); + } + + void Remove(const SourceDetails& details) override + { + UNREFERENCED_PARAMETER(details); + THROW_HR(E_NOTIMPL); + } + }; + } + + std::unique_ptr<ISourceFactory> PreIndexedSource::CreateFactory() + { + return std::make_unique<PreIndexedSourceFactory>(); + } + + const SourceDetails& PreIndexedSource::GetDetails() const + { + THROW_HR(E_NOTIMPL); + } + + SearchResult PreIndexedSource::Search(const SearchRequest& request) const + { + UNREFERENCED_PARAMETER(request); + THROW_HR(E_NOTIMPL); + } +} diff --git a/src/AppInstallerRepositoryCore/Microsoft/PreIndexedSource.h b/src/AppInstallerRepositoryCore/Microsoft/PreIndexedSource.h @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Public/AppInstallerRepositorySource.h" +#include "SourceFactory.h" + +#include <string_view> + +namespace AppInstaller::Repository::Microsoft +{ + // A source where the index is precomputed and stored on a server within an optional MSIX package. + // In addition, the manifest files are also individually available on the server. + struct PreIndexedSource : public ISource + { + PreIndexedSource(const PreIndexedSource&) = delete; + PreIndexedSource& operator=(const PreIndexedSource&) = delete; + + PreIndexedSource(PreIndexedSource&&) = default; + PreIndexedSource& operator=(PreIndexedSource&&) = default; + + // Get the type string for this source. + static constexpr std::string_view Type() + { + using namespace std::string_view_literals; + return "Microsoft.PreIndexed"sv; + } + + // Creates a source factory for this type. + static std::unique_ptr<ISourceFactory> CreateFactory(); + + // ISource + + // Get the source's details. + const SourceDetails& GetDetails() const override; + + // Execute a search on the source. + SearchResult Search(const SearchRequest& request) const override; + }; +} diff --git a/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySource.h b/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySource.h @@ -34,26 +34,30 @@ namespace AppInstaller::Repository // Interface for interacting with a source from outside of the repository lib. struct ISource { + virtual ~ISource() = default; + // Get the source's details. virtual const SourceDetails& GetDetails() const = 0; - // Request that the source update its internal data from the upstream location. - virtual void Update() = 0; - // Execute a search on the source. virtual SearchResult Search(const SearchRequest& request) const = 0; }; + // Gets the details for all sources. + std::vector<SourceDetails> GetSources(); + // Adds a new source for the user. - std::unique_ptr<ISource> AddSource(std::string name, std::string type, std::string arg); + void AddSource(std::string name, std::string type, std::string arg); // Opens an existing source. // Passing an empty string as the name of the source will return a source that aggregates all others. - std::unique_ptr<ISource> OpenSource(std::string_view name); + std::unique_ptr<ISource> OpenSource(std::string_view name = {}); - // Gets the details for all sources. - std::vector<SourceDetails> GetSources(); + // Updates an existing source. + // Return value indicates whether the named source was found. + bool UpdateSource(std::string_view name); // Removes an existing source. - void RemoveSource(std::string_view name); + // Return value indicates whether the named source was found. + bool RemoveSource(std::string_view name); } diff --git a/src/AppInstallerRepositoryCore/RepositorySource.cpp b/src/AppInstallerRepositoryCore/RepositorySource.cpp @@ -3,6 +3,8 @@ #include "pch.h" #include "Public/AppInstallerRepositorySource.h" +#include "SourceFactory.h" +#include "Microsoft/PreIndexedSource.h" namespace AppInstaller::Repository { @@ -147,17 +149,64 @@ namespace AppInstaller::Repository Runtime::SetSetting(settingName, out.c_str()); } + + // Finds a source from the given vector by its name. + auto FindSourceByName(std::vector<SourceDetails>& sources, std::string_view name) + { + return std::find_if(sources.begin(), sources.end(), [&name](const SourceDetails& sd) { return Utility::CaseInsensitiveEquals(sd.Name, name); }); + } + +#ifndef AICLI_DISABLE_TEST_HOOKS + static std::map<std::string, std::function<std::unique_ptr<ISourceFactory>()>> s_Sources_TestHook_SourceFactories; +#endif + + std::unique_ptr<ISourceFactory> GetFactoryForType(std::string_view type) + { +#ifndef AICLI_DISABLE_TEST_HOOKS + // Tests can ensure case matching + auto itr = s_Sources_TestHook_SourceFactories.find(std::string(type)); + if (itr != s_Sources_TestHook_SourceFactories.end()) + { + return itr->second(); + } +#endif + + // For now, enable an empty type to represent the only one we have. + if (type.empty() || + Utility::CaseInsensitiveEquals(Microsoft::PreIndexedSource::Type(), type)) + { + return Microsoft::PreIndexedSource::CreateFactory(); + } + + THROW_HR(APPINSTALLER_CLI_ERROR_INVALID_SOURCE_TYPE); + } + + std::unique_ptr<ISource> CreateSourceFromDetails(const SourceDetails& details) + { + return GetFactoryForType(details.Type)->Create(details); + } + + void UpdateSourceFromDetails(SourceDetails& details) + { + GetFactoryForType(details.Type)->Update(details); + } + + void RemoveSourceFromDetails(const SourceDetails& details) + { + GetFactoryForType(details.Type)->Remove(details); + } } - std::unique_ptr<ISource> AddSource(std::string name, std::string type, std::string arg) + void AddSource(std::string name, std::string type, std::string arg) { THROW_HR_IF(E_INVALIDARG, name.empty()); - THROW_HR_IF(E_INVALIDARG, type.empty()); + + AICLI_LOG(Repo, Info, << "Adding source: Name[" << name << "], Type[" << type << "], Arg[" << arg << "]"); // Check all sources for the given name. std::vector<SourceDetails> currentSources = GetSources(); - auto itr = std::find_if(currentSources.begin(), currentSources.end(), [&name](const SourceDetails& sd) { return Utility::CaseInsensitiveEquals(sd.Name, name); }); + auto itr = FindSourceByName(currentSources, name); THROW_HR_IF(APPINSTALLER_CLI_ERROR_SOURCE_NAME_ALREADY_EXISTS, itr != currentSources.end()); SourceDetails details; @@ -166,23 +215,50 @@ namespace AppInstaller::Repository details.Arg = std::move(arg); details.LastUpdateTime = Utility::ConvertUnixEpochToSystemClock(0); - // TODO: Implement actual source creation, for now we just add to the user setting. - // Ex. - //std::unique_ptr<ISource> result = CreateSourceFromDetails(details); + UpdateSourceFromDetails(details); + + AICLI_LOG(Repo, Info, << "Source created with extra data: " << details.Data); currentSources = GetSourcesFromSetting(s_RepositorySettings_UserSources); - // NOTE: When implementing creation as above, insert the details that we then get out of the result. - currentSources.push_back(details); + currentSources.emplace_back(details); SetSourcesToSetting(s_RepositorySettings_UserSources, currentSources); - - return {}; } std::unique_ptr<ISource> OpenSource(std::string_view name) { - UNREFERENCED_PARAMETER(name); - return {}; + if (name.empty()) + { + // TODO: Create aggregate source here. For now, just get the first in the list. + std::vector<SourceDetails> currentSources = GetSources(); + + if (currentSources.empty()) + { + AICLI_LOG(Repo, Info, << "Default source requested, but no sources configured"); + return {}; + } + else + { + AICLI_LOG(Repo, Info, << "Default source requested, using first source: " << currentSources[0].Name); + return CreateSourceFromDetails(currentSources[0]); + } + } + else + { + std::vector<SourceDetails> currentSources = GetSources(); + auto itr = FindSourceByName(currentSources, name); + + if (itr == currentSources.end()) + { + AICLI_LOG(Repo, Info, << "Named source requested, but not found: " << name); + return {}; + } + else + { + AICLI_LOG(Repo, Info, << "Named source requested, found: " << itr->Name); + return CreateSourceFromDetails(*itr); + } + } } std::vector<SourceDetails> GetSources() @@ -190,8 +266,61 @@ namespace AppInstaller::Repository return GetSourcesFromSetting(s_RepositorySettings_UserSources); } - void RemoveSource(std::string_view name) + bool UpdateSource(std::string_view name) + { + THROW_HR_IF(E_INVALIDARG, name.empty()); + + std::vector<SourceDetails> currentSources = GetSourcesFromSetting(s_RepositorySettings_UserSources); + auto itr = FindSourceByName(currentSources, name); + + if (itr == currentSources.end()) + { + AICLI_LOG(Repo, Info, << "Named source to be updated, but not found: " << name); + return false; + } + else + { + AICLI_LOG(Repo, Info, << "Named source to be updated, found: " << itr->Name); + UpdateSourceFromDetails(*itr); + + SetSourcesToSetting(s_RepositorySettings_UserSources, currentSources); + return true; + } + } + + bool RemoveSource(std::string_view name) + { + THROW_HR_IF(E_INVALIDARG, name.empty()); + + std::vector<SourceDetails> currentSources = GetSourcesFromSetting(s_RepositorySettings_UserSources); + auto itr = FindSourceByName(currentSources, name); + + if (itr == currentSources.end()) + { + AICLI_LOG(Repo, Info, << "Named source to be removed, but not found: " << name); + return false; + } + else + { + AICLI_LOG(Repo, Info, << "Named source to be removed, found: " << itr->Name); + RemoveSourceFromDetails(*itr); + + currentSources.erase(itr); + SetSourcesToSetting(s_RepositorySettings_UserSources, currentSources); + + return true; + } + } + +#ifndef AICLI_DISABLE_TEST_HOOKS + void TestHook_SetSourceFactoryOverride(const std::string& type, std::function<std::unique_ptr<ISourceFactory>()>&& factory) + { + s_Sources_TestHook_SourceFactories[type] = std::move(factory); + } + + void TestHook_ClearSourceFactoryOverrides() { - UNREFERENCED_PARAMETER(name); + s_Sources_TestHook_SourceFactories.clear(); } +#endif } diff --git a/src/AppInstallerRepositoryCore/SourceFactory.h b/src/AppInstallerRepositoryCore/SourceFactory.h @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include <Public/AppInstallerRepositorySource.h> + +#include <memory> + + +namespace AppInstaller::Repository +{ + // Interface for manipulating a source based on its details. + struct ISourceFactory + { + virtual ~ISourceFactory() = default; + + // Creates a source object from the given details. + virtual std::unique_ptr<ISource> Create(const SourceDetails& details) = 0; + + // Updates the source from the given details, writing back to the details any changes. + virtual void Update(SourceDetails& details) = 0; + + // Removes the source from the given details. + virtual void Remove(const SourceDetails& details) = 0; + }; +} diff --git a/src/AppInstallerRepositoryCore/pch.h b/src/AppInstallerRepositoryCore/pch.h @@ -31,3 +31,8 @@ #include <tuple> #include <type_traits> #include <utility> + +#ifndef AICLI_DISABLE_TEST_HOOKS +#include <functional> +#include <map> +#endif